JSON Backend

← serde

Introduction

xpp::serde::json::Serializer and json::Deserializer wrap libx/x/json/, providing a DOM-based JSON backend for the serde framework. The Serializer builds an xJson tree with xJsonNew* (malloc-backed) and dumps it via xJsonStringify. The Deserializer parses with xJsonParseCopy (arena-backed, safe) and walks the DOM.

Link target: any TU including json.h must link xjson.

Usage

Serialize

One-step (preferred for simple cases — mirrors serde_json::to_string):

#include <xpp/serde/json.h>

Person p{"Alice", 30};

auto r = xpp::serde::json::to_string(p);
if (r.is_ok()) {
  xpp::String json = std::move(r).unwrap();
  // json == R"({"name":"Alice","age":30})"
}

Two-step (when you need to inspect or reuse the Serializer):

xpp::serde::json::Serializer ser;
xpp::serde::serialize(p, ser);
xpp::String json = ser.to_string();

Serializer::to_string() stringifies the internal xJson tree to a compact JSON string. Call it after serialize() returns Ok. Serializer::reset() clears the internal state for reuse.

Deserialize

auto d_res = xpp::serde::json::Deserializer::from_string(R"({"name":"Bob","age":25})");
if (!d_res.is_ok()) {
  // Parse error (malformed JSON)
  xpp::serde::Error &e = d_res.unwrap_err();
  // e.kind == ErrorKind::InvalidValue, e.message has details
  return;
}
auto d = std::move(d_res).unwrap();

auto r = xpp::serde::deserialize<Person>(d);
if (r.is_ok()) {
  Person &p = r.unwrap();
  // p.name == "Bob", p.age == 25
} else {
  xpp::serde::Error &e = r.unwrap_err();
  // e.g. MissingField, InvalidValue
}

Entry points:

MethodInput
Deserializer::from_string(const xpp::String&)Owned String
Deserializer::from_string(const char*)C string literal

Both parse a copy into an arena — the input does not need to outlive the Deserializer.

Encoding

JSON is self-describing — field names are encoded as object keys, and Option<T> uses null for None. No special discriminator bytes are needed for Some; serialize_some is a plain forward to serde::serialize.

Primitives

C++ typeJSON
booltrue / false
int32_t / int64_tnumber
uint32_t / uint64_tnumber
float / doublenumber (NaN/Inf rejected on serialize)
xpp::String"..."

Composite types

C++ typeJSON
Option<T> (None)null
Option<T> (Some)value as-is (no wrapper)
Vec<T>[...]
struct (via XPP_SERDE){"field": ...}
Enum (external){"tagString": {payload}}
Enum (adjacent){"tag": "tagString", "content": {payload}}

Example — a struct with Option and Vec:

struct Group {
  xpp::String       name;
  Option<int32_t>   priority;   // null if not set
  Vec<xpp::String>  tags;
};
XPP_SERDE(Group, (name), (priority), (tags))

Group{"infra", none, Vec<String>{"a","b"}} serializes to:

{"name":"infra","priority":null,"tags":["a","b"]}

Tagged variants

StrategyJSON shape
External{"circle": {"r": 1.0}}
Adjacent{"tag": "circle", "content": {"r": 1.0}}

See the serde README for the macro declarations.

Unknown Fields

The JSON deserializer skips unknown fields by default — if the JSON object contains keys that the Deserialize<T> visitor doesn't recognize, they are ignored via next_value_ignored(). This makes the format forward-compatible: adding a field to the JSON (e.g. from a newer server) does not break older clients.

If you need strict mode (reject unknown fields), hand-write the Deserialize<T> specialization and return Err from the else branch instead of calling next_value_ignored().

Error Handling

All errors surface as Result<T, Error>. Common JSON-specific failures:

ScenarioErrorKindExample
Malformed JSON at parse timeInvalidValuefrom_string("{bad}")
Type mismatchInvalidValueexpecting i32, got "hello"
Required field missingMissingField{"name":"X"} into a struct requiring age
Unknown variant tagUnknownField{"triangle":{...}} when only circle/square declared
NaN/Inf in f64InvalidValueserialize(1.0/0.0, ser)

Errors are non-fatal — the Deserializer can be reused after an error on a different input (call from_string again).