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 Proto | JSON |
|---|---|
Void | null |
Bool | true / false |
Int8/Int16/Int32, UInt8/UInt16/UInt32 | number |
Int64, UInt64 | string holding a decimal integer |
Float32, Float64 | number, except NaN, Infinity and -Infinity, which are strings |
Text | string |
Data | array of byte-valued numbers, unless $Json.base64 or $Json.hex is applied |
| enum | string naming the enumerant, or a number if the ordinal is not in the schema |
| struct, group | object |
List(T) | array |
AnyPointer, interface | not 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_schemachecks 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 aDatafield (or the elements of a list ofData) 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 notData.
§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:
Codec::with_field_override— for one specific field of one specific struct.Codec::with_type_override— for every field of a given type.Codec::with_named_codec— for every field or struct tagged$Rust.codec("name")in the schema, which keeps the choice next to the data it applies to.
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
Dataelement above 255 is rejected. C++ intends the same — its error says “not an integer in [0, 255]” — but implements the bound asbyte(x) == x, whose out-of-rangedouble-to-byteconversion is undefined behaviour; in practice it accepts256,300and511and stores them modulo 256. Being stricter cannot break round-tripping, since the C++ encoder never emits such a value. \uXXXXsurrogate 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 RustStringcannot 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/UInt64accept 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
Displayform, which never uses exponent notation:1e300is emitted as 301 digits rather than as1e300. 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.
- Codec
Options - Encoding and decoding options for a
Codec.
Enums§
- Json
Value - An in-memory JSON value.
Traits§
- Field
Codec - 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
FieldCodecfrom 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.