Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,21 @@ sonic_json::WriteBuffer wb;
doc.Serialize(wb);
std::cout << wb.ToString() << std::endl;
```

#### Pretty serialize to an indented string
Use `PrettySerialize()` when human-readable output is preferred. It uses two
spaces per indentation level by default; pass a different width as the second
argument when needed.
```c++
#include "sonic/sonic.h"
// ...
sonic_json::WriteBuffer wb;
doc.PrettySerialize(wb); // two spaces per level
std::cout << wb.ToString() << std::endl;

doc.PrettySerialize(wb, 4); // four spaces per level
std::string json = doc.PrettyDump(4);
```
### Node
Node is the present for JSON value and supports all JSON value manipulation.

Expand Down
5 changes: 5 additions & 0 deletions include/sonic/dom/dynamicnode.h
Original file line number Diff line number Diff line change
Expand Up @@ -980,6 +980,11 @@ class DNode : public GenericNode<DNode<Allocator>> {
return internal::SerializeImpl<serializeFlags>(this, wb);
}

template <SerializeFlags serializeFlags = SerializeFlags::kSerializeDefault>
SonicError prettySerializeImpl(WriteBuffer& wb, size_t indentSize) const {
return internal::PrettySerializeImpl<serializeFlags>(this, wb, indentSize);
}

sonic_force_inline DNode* nextImpl() { return this + 1; }

sonic_force_inline const DNode* cnextImpl() const { return this + 1; }
Expand Down
30 changes: 30 additions & 0 deletions include/sonic/dom/genericnode.h
Original file line number Diff line number Diff line change
Expand Up @@ -1069,6 +1069,19 @@ class GenericNode {
return downCast()->template serializeImpl<serializeFlags>(wb);
}

/**
* @brief serialize this node as an indented JSON string.
* @param serializeFlags combination of different SerializeFlag.
* @param wb write buffer where you want to store the JSON string.
* @param indentSize number of spaces to use for each indentation level.
* @return EndcodeError
*/
template <SerializeFlags serializeFlags = SerializeFlags::kSerializeDefault>
SonicError PrettySerialize(WriteBuffer& wb, size_t indentSize = 2) const {
return downCast()->template prettySerializeImpl<serializeFlags>(wb,
indentSize);
}

/**
* @brief dump this node as json string.
* @param serializeFlags combination of different SerializeFlag.
Expand All @@ -1085,6 +1098,23 @@ class GenericNode {
return std::string(sv.data(), sv.size());
}

/**
* @brief dump this node as an indented JSON string.
* @param serializeFlags combination of different SerializeFlag.
* @param indentSize number of spaces to use for each indentation level.
* @return empty string if there are errors when serializing.
*/
template <SerializeFlags serializeFlags = SerializeFlags::kSerializeDefault>
std::string PrettyDump(size_t indentSize = 2) const {
WriteBuffer wb;
SonicError err = PrettySerialize<serializeFlags>(wb, indentSize);
if (err != kErrorNone) {
return "";
}
auto sv = wb.ToStringView();
return std::string(sv.data(), sv.size());
}

protected:
sonic_force_inline NodeType* next() noexcept {
return downCast()->nextImpl();
Expand Down
97 changes: 97 additions & 0 deletions include/sonic/dom/serialize.h
Original file line number Diff line number Diff line change
Expand Up @@ -227,5 +227,102 @@ sonic_force_inline SonicError SerializeImpl(const NodeType* node,
return kSerErrorInvalidObjKey;
}

inline void PushPrettyLine(WriteBuffer& wb, size_t depth, size_t indent_size) {
wb.Push('\n');
for (size_t i = 0; i < depth; ++i) {
for (size_t j = 0; j < indent_size; ++j) {
wb.Push(' ');
}
}
}

template <SerializeFlags serializeFlags, typename NodeType>
SonicError PrettySerializeImpl(const NodeType* node, WriteBuffer& wb,
size_t indent_size) {
struct ParentCtx {
const NodeType* node;
size_t index;
};

constexpr SerializeFlags append_flags =
serializeFlags | SerializeFlags::kSerializeAppendBuffer;
internal::Stack parents;
size_t depth = 0;
SonicError error = kErrorNone;

if constexpr ((serializeFlags & SerializeFlags::kSerializeAppendBuffer) ==
0) {
wb.Clear();
}
wb.Reserve(wb.Size() + 64);

value_begin:
if (!node->IsContainer() || node->Empty()) {
error = SerializeImpl<append_flags>(node, wb);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid constructing a serializer stack for every leaf

For every scalar or empty-container value, this invokes SerializeImpl, which unconditionally constructs and destroys its own heap-backed internal::Stack even though leaf serialization never needs that stack; object keys incur the same cost through the other calls below. Consequently, pretty-printing an N-element primitive array performs O(N) avoidable heap allocation/free pairs, which makes this serialization path scale poorly; emit leaves through a reusable scalar helper or shared traversal state instead.

AGENTS.md reference: AGENTS.md:L569-L577

Useful? React with 👍 / 👎.

if (sonic_unlikely(error != kErrorNone)) {
return error;
}
goto value_end;
}

wb.Push(node->IsObject() ? '{' : '[');
parents.Push(ParentCtx{node, 0});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Propagate parent-stack allocation failures

When a nonempty document is deeply nested enough that parents must grow, or its initial allocation fails, Stack::Reserve silently leaves the capacity unchanged but this Push still writes and advances top_. Under allocation pressure, PrettySerialize can therefore write out of bounds or crash instead of returning kErrorNoMem; use a checked stack operation and propagate failure before writing.

AGENTS.md reference: AGENTS.md:L137-L146

Useful? React with 👍 / 👎.

++depth;
PushPrettyLine(wb, depth, indent_size);

if (node->IsObject()) {
auto member = node->MemberBegin();
if (sonic_unlikely(!member->name.IsString())) {
return kSerErrorInvalidObjKey;
}
error = SerializeImpl<append_flags>(&(member->name), wb);
if (sonic_unlikely(error != kErrorNone)) {
return error;
}
wb.Push(':');
wb.Push(' ');
node = &(member->value);
} else {
node = &(*(node->Begin()));
}
goto value_begin;

value_end:
if (parents.Empty()) {
return kErrorNone;
}

{
ParentCtx* parent = parents.Top<ParentCtx>();
++parent->index;
if (parent->index < parent->node->Size()) {
wb.Push(',');
PushPrettyLine(wb, depth, indent_size);
if (parent->node->IsObject()) {
auto member = parent->node->MemberBegin() + parent->index;
if (sonic_unlikely(!member->name.IsString())) {
return kSerErrorInvalidObjKey;
}
error = SerializeImpl<append_flags>(&(member->name), wb);
if (sonic_unlikely(error != kErrorNone)) {
return error;
}
wb.Push(':');
wb.Push(' ');
node = &(member->value);
} else {
node = &(*(parent->node->Begin() + parent->index));
}
goto value_begin;
}

--depth;
PushPrettyLine(wb, depth, indent_size);
wb.Push(parent->node->IsObject() ? '}' : ']');
parents.Pop<ParentCtx>(1);
}
goto value_end;
}

} // namespace internal
} // namespace sonic_json
64 changes: 64 additions & 0 deletions tests/document_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -577,6 +577,70 @@ TYPED_TEST(DocumentTest, SerializeOK) {
}
}

TYPED_TEST(DocumentTest, PrettySerialize) {
TypeParam doc;
const std::string json =
R"({"name":"sonic","items":[1,true,null,{"nested":"value"}],"empty":[]})";
doc.Parse(json);
ASSERT_FALSE(doc.HasParseError());

const std::string expected =
"{\n"
" \"name\": \"sonic\",\n"
" \"items\": [\n"
" 1,\n"
" true,\n"
" null,\n"
" {\n"
" \"nested\": \"value\"\n"
" }\n"
" ],\n"
" \"empty\": []\n"
"}";

WriteBuffer wb;
EXPECT_EQ(doc.PrettySerialize(wb), kErrorNone);
EXPECT_EQ(wb.ToStringView(), expected);
EXPECT_EQ(doc.PrettyDump(), expected);

const std::string four_space_expected =
"{\n"
" \"value\": 1\n"
"}";
TypeParam small_doc;
small_doc.Parse(R"({"value":1})");
ASSERT_FALSE(small_doc.HasParseError());
EXPECT_EQ(small_doc.PrettyDump(4), four_space_expected);
}

TYPED_TEST(DocumentTest, PrettySerializeFlagsAndErrors) {
TypeParam doc;
doc.Parse(R"([1,2])");
ASSERT_FALSE(doc.HasParseError());

WriteBuffer wb;
wb.Push("prefix:", 7);
EXPECT_EQ(
doc.template PrettySerialize<SerializeFlags::kSerializeAppendBuffer>(wb,
0),
kErrorNone);
EXPECT_STREQ(wb.ToString(), "prefix:[\n1,\n2\n]");

doc.SetDouble(std::numeric_limits<double>::infinity());
EXPECT_EQ(doc.PrettySerialize(wb), kSerErrorInfinity);
EXPECT_TRUE(doc.PrettyDump().empty());
EXPECT_EQ(doc.template PrettyDump<SerializeFlags::kSerializeInfNan>(),
"\"Infinity\"");

doc.Parse(R"({"key":1})");
ASSERT_FALSE(doc.HasParseError());
using DNode = typename TypeParam::NodeType;
auto member = doc.MemberBegin();
const_cast<DNode*>(&(member->name))->SetNull();
EXPECT_EQ(doc.PrettySerialize(wb), kSerErrorInvalidObjKey);
EXPECT_TRUE(doc.PrettyDump().empty());
}

TYPED_TEST(DocumentTest, SonicErrorInvalidKey) {
using DNode = typename TypeParam::NodeType;
auto iter = this->doc_.MemberBegin();
Expand Down