Skip to main content

Crate capnp_json

Crate capnp_json 

Source
Expand description

A Cap’n Proto JSON codec, implementing the codec defined in json.capnp.

The wire format is compatible with the C++ capnp::JsonCodec that ships with Cap’n Proto: messages encoded by this crate can be decoded by the C++ codec, and vice-versa. Encoding and decoding are driven entirely by the schema’s runtime type information, so no per-type derive or code generation beyond capnpc is required.

§Quick start

to_json turns any struct reader into a JSON string, and from_json fills a struct builder from one:

use capnp::message;
use capnp_json::{from_json, to_json};

let mut builder = message::Builder::new_default();
let root: my_schema_capnp::my_struct::Builder<'_> = builder.init_root();
// ... populate `root` ...

let json: String = to_json(root.reborrow_as_reader())?;

let mut decoded = message::Builder::new_default();
let decoded_root: my_schema_capnp::my_struct::Builder<'_> =
  decoded.init_root();
from_json(&json, decoded_root)?;

Both are thin wrappers around Codec, which is what you need as soon as you want custom encodings for particular fields or types — see custom codecs below.

§Type mapping

Cap’n ProtoJSON
Voidnull
Booltrue / false
Int8/Int16/Int32, UInt8/UInt16/UInt32number
Int64, UInt64string holding a decimal integer
Float32, Float64number, except NaN, Infinity and -Infinity, which are strings
Textstring
Dataarray of byte-valued numbers, unless $Json.base64 or $Json.hex is applied
enumstring naming the enumerant, or a number if the ordinal is not in the schema
struct, groupobject
List(T)array
AnyPointer, interfacenot representable; an error unless a custom codec is registered

64-bit integers are strings because JSON numbers are IEEE-754 doubles and cannot represent the full 64-bit range exactly. When decoding, both the string and the number form are accepted for Int64/UInt64.

Integer fields are range-checked on decode, as they are in C++: a number outside the field’s range, or one with a fractional part, is an error rather than being silently clamped or truncated. 300 is not an Int8 and 1.9 is not an Int32. The same applies to the elements of a Data array, which must be whole numbers in [0, 255].

Float fields are not range-checked, also matching C++: a magnitude too large for a Float32 becomes an infinity. Enum ordinals given as numbers are not checked either.

On encode, a field is omitted entirely when it is unset — for pointer fields (text, data, lists, structs) that means a null pointer. On decode, a field absent from the JSON is left at its schema default, and a JSON field that does not correspond to anything in the schema is ignored.

A null pointer and an absent field are the same thing in Cap’n Proto, so a JSON null for a Text, Data, List or struct field means the field was not set, rather than that it holds an empty value; the field is left alone. Void is the exception, since null is its value rather than its absence, and Float32/Float64 read null as NaN. For every other type null is an error. This is only true of fields: as a list element, null has to be a value, so [null] is an error for a list of text. All of this matches the C++ codec (isPointerToJsonNull), though that behaviour is on its main branch and not in any release yet.

§JSON annotations

To use any of the JSON annotations defined in json.capnp, tell capnpc to resolve references to the annotation schema to this crate from your build.rs:

capnpc::CompilerCommand::new()
    .crate_provides("capnp_json", [0x8ef99297a43a5e34])
    .file("my_schema.capnp")
    .run()
    .expect("compiling schema");

0x8ef99297a43a5e34 is the file ID of json.capnp. The supported annotations are:

  • $Json.name("...") — use a different name for a field, enumerant, group or union member in the JSON representation.

  • $Json.flatten() / $Json.flatten(prefix = "p.") — splice a struct, group or union’s members directly into the parent object rather than nesting them, optionally prefixing each name.

    Because a flattened field consumes no JSON nesting, flattening must terminate: a struct cannot flatten a field of its own type, directly or through a chain of other flattened fields and groups. validate_schema checks this and reports it the way the C++ codec does; encoding and decoding do not, since a schema is compile-time data and a cycle in one is a build-time mistake rather than a property of any input. Left unchecked, a cyclic schema is still rejected when decoded, but by the recursion limit and with a less pointed message.

  • $Json.discriminator(name = "kind", valueName = "value") — encode which member of a union is active as a sibling string field rather than by the presence of the member’s own key.

  • $Json.base64 / $Json.hex — encode a Data field (or the elements of a list of Data) as a Base64 or hex string instead of an array of byte values. Applying both to one field is an error, as is applying either to a field that is not Data.

§Custom codecs

Some things have no natural JSON form — AnyPointer and interface fields most obviously, but also domain types such as timestamps that you would rather see as an ISO-8601 string than as a struct. Codec lets you supply a FieldCodec for these, bound in one of three ways:

The $Rust.codec annotation is defined in this crate’s own rust-json.capnp (file ID 0xf955e504bf781ac6); see Codec::with_named_codec for the build.rs setup it needs.

§Compatibility notes

The following are known divergences from the C++ codec. They matter mostly when decoding input from an untrusted or third-party producer:

  • Input after the top-level value is ignored rather than rejected.
  • A Data element above 255 is rejected. C++ intends the same — its error says “not an integer in [0, 255]” — but implements the bound as byte(x) == x, whose out-of-range double-to-byte conversion is undefined behaviour; in practice it accepts 256, 300 and 511 and stores them modulo 256. Being stricter cannot break round-tripping, since the C++ encoder never emits such a value.
  • \uXXXX surrogate pairs are combined into the character they denote. C++ decodes each escape separately and produces WTF-8 — the surrogates encoded individually, which is not valid UTF-8 and which a Rust String cannot hold — and notes as much in a TODO. Unpaired surrogates are rejected here rather than replaced. This cannot affect round-tripping: the C++ encoder writes non-BMP characters as literal UTF-8, never as escapes.
  • Only Int64/UInt64 accept the string form of an integer on decode; C++ accepts it for every integer width.
  • Duplicate keys within one JSON object are rejected.
  • Floats are written in Rust’s Display form, which never uses exponent notation: 1e300 is emitted as 301 digits rather than as 1e300. Both parse back to the same value.

Output is not pretty-printed, and the Value / Call / raw extensions from json.capnp are not implemented.

Structs§

Codec
A JSON codec for Cap’n Proto messages.
CodecOptions
Encoding and decoding options for a Codec.

Enums§

JsonValue
An in-memory JSON value.

Traits§

FieldCodec
A custom JSON representation for a single Cap’n Proto value.

Functions§

from_json
Decode a JSON string into a Cap’n Proto struct builder.
make_field_codec
Build a FieldCodec from an encoder and a decoder closure.
to_json
Encode a Cap’n Proto struct as a JSON string.
validate_schema
Check that a schema can be represented as JSON at all.