JSON Format
Both C++ and Rust services support typed JSON serialization. C++ services use psio::to_json and psio::from_json. Rust services use serde_json.
Structs
Both psio and serde_json represent structs with named fields as JSON objects.
Tuples
Both psio and serde_json represent tuples as JSON arrays. The empty tuple has a problem. psio renders std::tuple<>{} as you'd expect: []. serde_json, however, renders (), the unit, as null. See Empty, below, for a workaround.
- TODO: psio json support for tuples
Unit Structs (Rust)
serde_json represents unit structs (like below) as null. We recommend against using this; fracpack and schema don't support it. See Empty, below, for an alternative.
struct MyUnit;
Tuple Structs (Rust)
serde_json represents these structs as tuples. Beware of the special cases.
struct Empty(); // []
struct TupleOfOne((u32,)); // [value]
struct TupleOfTwo(u32, String); // [value, value]
struct TupleOfThree(u32, String, f64); // [value, value, value]
Special Cases (Rust)
serde_json represents these structs as their inner values instead of as tuples ([...]). Fracpack and Schema act the same.
struct Single1(u32); // 1234
struct Single2(String, ); // "A string"
Numbers
64-bit Numbers are incredibly tricky in JSON, thanks in part to JavaScript, and thanks in part to common JSON libraries in type-safe languages.
- JavaScript's number type can handle integers up to 53 bits unsigned, 54 signed. Extra precision is silently truncated. e.g.
10000000000000001 == 10000000000000000. - JavaScript's BigInt type supports arbitrary precision, but JavaScript's built-in JSON conversions don't support it.
The cleanest workaround seems to be to store 64-bit integers in quoted strings, but several widely-used JSON libraries in type-safe languages decided against that workaround, and reject incoming quoted numbers. serde_json used to support it (input only), but hit some nasty conflicts and had to remove it. serde_json provides customization (serialize_with and deserialize_with), but that gets cumbersome in nested types, e.g. Option<Vec<u64>>.
Instead of trying to get type-safe JSON libraries to work around JavaScript's limitations, it's probably time we ask JavaScript to pull its own weight. JavaScript JSON Libraries exist which handle BigInt.
psio::to_json (C++) does not place 64-bit numbers in quoted strings, but for a time psio::from_json will accept numbers in quoted strings for backwards compatibility. serde_json (Rust) does not place or accept numbers in quoted strings, unless you use customization. JavaScript needs a JSON library or will silently truncate values.
- TODO: update
psio::to_jsonto not quote numbers - TODO: Rethink GraphQL numeric handling. It currently relies on
to_json. - TODO: update JavaScript code
Strings
psio::to_json and psio::from_json use JSON strings for std::string. serde_json uses JSON strings for rust's various string types.
Optional
Both psio (std::optional) and serde_json (Option) represent the empty case as null and the non-empty case as the inner type.
Vectors and Arrays
Both psio and serde_json represent vectors (std::Vector, Vec) and arrays (std::array, [] (Rust)) as JSON arrays.
Byte vectors and arrays
Byte vectors and arrays create a tricky problem. The JSON array-of-numbers representation is wasteful and annoying for this; hex strings are more compact and readable. Base-64 is even more compact, but it is near impossible to read and there are 7 standard RFC encodings, plus more.
When should a vector or array use a hex representation? It's convenient to automatically switch when the element type is an 8-bit number, but this can be jarring for new service developers who don't expect it. We could have a separate hex type, but that's annoying for experienced developers in both Rust and C++. serdes already made a choice: vectors by default use the array notation in JSON. serialize_with and deserialize_with can opt into other representations, but they are cumbersome in nested types, e.g. Option<Vec<u8>>. That leaves a separate hex type.
The Rust psibase library provides this wrapper type, which uses hex strings as its JSON format:
// T must be `Vec<u8>`, `&[u8]`, or `[u8; SIZE]`
pub struct Hex<T>(pub T);
- TODO: C++: A fixed-size hex type
- TODO: C++: Switch existing byte vectors and arrays within psibase structs to hex wrappers
- TODO: C++: Remove hex representation from
std::vector<*>andstd::array<*> - TODO: C++: fracpack support for encoding
psio::bytesas a vector instead of a struct of vector - TODO: C++: fracpack support for encoding new fixed-size hex type as an array instead of a struct of array
Variants / Enums
There are probably as many ways to represent these in JSON as there are grains of sand on the beach. serde_json supports 4 approaches. Of these, only the externally tagged and adjacently tagged representations cover all situations unambiguously.
- TODO: pick one. It will be easier on rust devs if we choose serde_json's default (externally tagged). It looks like I can take advantage of the syntax of the externally-tagged option to represent nested types in the schema without falling back on a DSL.