Skip to main content

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}