Code generators include a JSON serializer which will convert a target language's representation of Stone data types into JSON. This document explores how Stone data types, regardless of language, are mapped to JSON.
Stone Primitive | JSON Representation |
---|---|
Boolean | Boolean |
Bytes | String: Base64-encoded |
Float{32,64} | Number |
Int{32,64}, UInt{32,64} | Number |
List | Array |
String | String |
Timestamp | String: Encoded using strftime() based on the Timestamp's format argument. |
Void | Null |
A struct is represented as a JSON object. Each specified field has a key in the object. For example:
struct Coordinate x Int64 y Int64
converts to:
{ "x": 1, "y": 2 }
If an optional (has a default or is nullable) field is not specified, the key should be omitted. For example, given the following spec:
struct SurveyAnswer age Int64 name String = "John Doe" address String?
If name
and address
are unset and age
is 28, then the struct
serializes to:
{ "age": 28 }
Setting name
or address
to null
is not a valid serialization;
deserializers will raise an error.
An explicit null
is allowed for fields with nullable types. While it's
less compact, this makes serialization easier in some languages. The previous
example could therefore be represented as:
{ "age": 28, "address": null }
A struct that enumerates subtypes serializes similarly to a regular struct,
but includes a .tag
key to distinguish the type. Here's an example to
demonstrate:
struct A union* b B c C w Int64 struct B extends A x Int64 struct C extends A y Int64
Serializing A
when it contains a struct B
(with values of 1
for
each field) appears as:
{ ".tag": "b", "w": 1, "x": 1 }
If the recipient receives a tag it cannot match to a type, it should fallback to the parent type if it's specified as a catch-all.
For example:
{ ".tag": "d", "w": 1, "z": 1 }
Because d
is unknown, the recipient checks that struct A
is a
catch-all. Since it is, it deserializes the message to an A
object.
Similar to an enumerated subtype struct, recipients should check the .tag
key to determine the union variant.
Let's use the following example to illustrate how a union is serialized based on the selected variant:
union U singularity number Int64 coord Coordinate? infinity Infinity struct Coordinate x Int64 y Int64 union Infinity positive negative
The serialization of U
with tag singularity
is:
{ ".tag": "singularity" }
For a union member of primitive type (number
in the example), the
serialization is as follows:
{ ".tag": "number", "number": 42 }
Note that number
is used as the value for .tag
and as a key to hold
the value. This same pattern is used for union members with types that are
other unions or structs with enumerated subtypes.
Union members that are ordinary structs (coord
in the example) serialize
as the struct with the addition of a .tag
key. For example, the
serialization of Coordinate
is:
{ "x": 1, "y": 2 }
The serialization of U
with tag coord
is:
{ ".tag": "coord", "x": 1, "y": 2 }
The serialization of U
with tag infinity
is nested:
{ ".tag": "infinity", "infinity": { ".tag": "positive" } }
The same rule applies for members that are enumerated subtypes.
Note that coord
references a nullable type. If it's unset, then the
serialization only includes the tag:
{ ".tag": "coord" }
You may notice that if Coordinate
was defined to have no fields, it is
impossible to differentiate between an unset value and a value of coordinate.
In these cases, we prescribe that the deserializer should return a null
or unset value.
Deserializers should support an additional representation of void union
members: the tag itself as a string. For example, tag singularity
could
be serialized as simply:
"singularity"
This is convenient for humans manually entering the argument, allowing them to avoid typing an extra layer of JSON object nesting.