capnp_json/lib.rs
1//! A [Cap'n Proto](https://capnproto.org) JSON codec, implementing the codec
2//! defined in [`json.capnp`].
3//!
4//! The wire format is compatible with the C++ `capnp::JsonCodec` that ships
5//! with Cap'n Proto: messages encoded by this crate can be decoded by the C++
6//! codec, and vice-versa.
7//!
8//! # Quick start
9//!
10//! ```ignore
11//! use capnp::message;
12//! use capnp_json::{from_json, to_json};
13//!
14//! let mut builder = message::Builder::new_default();
15//! let root: my_schema_capnp::my_struct::Builder<'_> = builder.init_root();
16//! // ... populate `root` ...
17//!
18//! let json: String = to_json(root.reborrow_as_reader())?;
19//!
20//! let mut decoded = message::Builder::new_default();
21//! let decoded_root: my_schema_capnp::my_struct::Builder<'_> =
22//! decoded.init_root();
23//! from_json(&json, decoded_root)?;
24//! ```
25//!
26//! # JSON annotations
27//!
28//! To use any of the JSON annotations defined in [`json.capnp`] (for example
29//! `$Json.name`, `$Json.flatten`, `$Json.discriminator`, `$Json.base64`,
30//! `$Json.hex`), tell `capnpc` to resolve references to the annotation schema
31//! to this crate from your `build.rs`:
32//!
33//! ```ignore
34//! capnpc::CompilerCommand::new()
35//! .crate_provides("capnp_json", [0x8ef99297a43a5e34])
36//! .file("my_schema.capnp")
37//! .run()
38//! .expect("compiling schema");
39//! ```
40//!
41//! [`json.capnp`]: https://github.com/capnproto/capnproto/blob/master/c%2B%2B/src/capnp/compat/json.capnp
42
43mod data;
44mod decode;
45mod encode;
46
47mod schema {
48 capnp::generated_code!(pub mod json_capnp);
49}
50
51#[doc(hidden)]
52pub use schema::json_capnp;
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
55enum DataEncoding {
56 #[default]
57 Default,
58 Base64,
59 Hex,
60}
61
62#[derive(Debug)]
63struct EncodingOptions<'schema, 'prefix> {
64 prefix: &'prefix std::borrow::Cow<'schema, str>,
65 name: &'schema str,
66 flatten: Option<json_capnp::flatten_options::Reader<'schema>>,
67 discriminator: Option<json_capnp::discriminator_options::Reader<'schema>>,
68 data_encoding: DataEncoding,
69}
70
71impl<'schema, 'prefix> EncodingOptions<'schema, 'prefix> {
72 fn from_field(
73 prefix: &'prefix std::borrow::Cow<'schema, str>,
74 field: &capnp::schema::Field,
75 ) -> capnp::Result<Self> {
76 let mut options = Self {
77 prefix,
78 name: field.get_proto().get_name()?.to_str()?,
79 flatten: None,
80 discriminator: None,
81 data_encoding: DataEncoding::Default,
82 };
83
84 for anno in field.get_annotations()?.iter() {
85 match anno.get_id() {
86 json_capnp::name::ID => {
87 options.name = anno
88 .get_value()?
89 .downcast::<capnp::text::Reader>()
90 .to_str()?;
91 }
92 json_capnp::base64::ID => {
93 if options.data_encoding != DataEncoding::Default {
94 return Err(capnp::Error::failed(
95 "Cannot specify both base64 and hex annotations on the same field"
96 .into(),
97 ));
98 }
99 options.data_encoding = DataEncoding::Base64;
100 }
101 json_capnp::hex::ID => {
102 if options.data_encoding != DataEncoding::Default {
103 return Err(capnp::Error::failed(
104 "Cannot specify both base64 and hex annotations on the same field"
105 .into(),
106 ));
107 }
108 options.data_encoding = DataEncoding::Hex;
109 }
110 json_capnp::flatten::ID => {
111 options.flatten = Some(
112 anno
113 .get_value()?
114 .downcast_struct::<json_capnp::flatten_options::Owned>(),
115 );
116 }
117 json_capnp::discriminator::ID => {
118 options.discriminator = Some(
119 anno
120 .get_value()?
121 .downcast_struct::<json_capnp::discriminator_options::Owned>(),
122 );
123 }
124 _ => {}
125 }
126 }
127 if options.data_encoding != DataEncoding::Default {
128 let mut element_type = field.get_type();
129 while let capnp::introspect::TypeVariant::List(sub_element_type) =
130 element_type.which()
131 {
132 element_type = sub_element_type;
133 }
134 if !matches!(element_type.which(), capnp::introspect::TypeVariant::Data) {
135 return Err(capnp::Error::failed(
136 "base64/hex annotation can only be applied to Data fields".into(),
137 ));
138 }
139 }
140 Ok(options)
141 }
142}
143
144/// Encode a Cap'n Proto value as a JSON string.
145///
146/// `reader` accepts anything that converts into a [`capnp::dynamic_value::Reader`]
147/// — typically a struct reader obtained from `message::Reader::get_root()` or
148/// `message::Builder::reborrow_as_reader()`.
149///
150/// `Int64`, `UInt64`, and non-finite floats are encoded as JSON strings, and
151/// `Data` fields are encoded as JSON arrays of bytes by default. The
152/// `$Json.base64` and `$Json.hex` annotations override the `Data` encoding,
153/// and the `$Json.name`, `$Json.flatten`, and `$Json.discriminator`
154/// annotations affect the layout of object fields and unions, all matching
155/// the C++ `capnp::JsonCodec` behaviour.
156pub fn to_json<'reader>(
157 reader: impl Into<capnp::dynamic_value::Reader<'reader>>,
158) -> capnp::Result<String> {
159 let mut writer = std::io::Cursor::new(Vec::with_capacity(4096));
160 encode::serialize_json_to(&mut writer, reader)?;
161 String::from_utf8(writer.into_inner()).map_err(|e| e.into())
162}
163
164/// Decode a JSON string into a Cap'n Proto struct builder.
165///
166/// `builder` accepts anything that converts into a [`capnp::dynamic_value::Builder`];
167/// the top-level value must be a struct builder, since JSON objects map to
168/// Cap'n Proto structs. The fields and annotations supported are the same as
169/// in [`to_json`].
170///
171/// Returns an error if `json` is malformed, if the top-level JSON value is
172/// not an object, or if any field's value cannot be coerced to its declared
173/// Cap'n Proto type.
174pub fn from_json<'segments>(
175 json: &str,
176 builder: impl Into<capnp::dynamic_value::Builder<'segments>>,
177) -> capnp::Result<()> {
178 let capnp::dynamic_value::Builder::Struct(builder) = builder.into() else {
179 return Err(capnp::Error::failed(
180 "Top-level JSON value must be an object".into(),
181 ));
182 };
183 decode::parse(json, builder)
184}