hd-lang

#Json

Status: standard library specification draft.

This chapter defines std.json, which lib/std writes in ordinary hd over the language tier:

  • the value type Json, and Number, which keeps 64-bit integers exact;
  • parse, a strict RFC 8259 parser;
  • JsonError, the error that parse and decode return;
  • the compact Display text of a Json, and its pretty method;
  • the typed part: JSON as a format over opt-in serialization, with to_json, from_json, encode, encode_pretty, and decode.

Nothing in it is language tier.

  1. std-json.import.corestd.json declares Json, Number, JsonError, and parse. None is a prelude name; code imports them, as in use std.json.parse.
  2. std-json.import.formatstd.json also declares to_json, from_json, encode, and decode. None is a prelude name either.
  3. std-json.import.prettystd.json also declares encode_pretty, which is not a prelude name.
  4. std-json.no-panicNone of these functions panics.
use std.json.{Json, JsonError, parse}

fn read(text: string) -> Result[Json, JsonError]:
    parse(text)   # a bad text is an .Err: no call here panics

#Json Values

A Json is one of six kinds:

pub enum Json:
    Null
    Bool(value: bool)
    Number(value: Number)
    Text(value: string)
    Array(items: List[Json])
    Object(fields: JsonObject)
  1. std-json.value.declstd.json declares the enum Json with exactly the variants above.
  2. std-json.value.object-typeAn Object holds a JsonObject, which keeps its members in insertion order. It is not a Map, whose iteration order is unspecified.
  3. std-json.value.eqJson implements Eq. Two values are equal when they are the same variant with equal contents.
  4. std-json.value.eq.arrayArrays are equal when they have equal items in the same order.
  5. std-json.value.eq.objectObjects are equal when they have the same keys with equal values. Their order does not matter.
  6. std-json.value.debugJson, Number, and JsonError implement Debug, as std-format.debug.std-types requires.

The accessors read one kind and give .None for any other:

Rule Method Result
std-json.value.accessors.getget pub fn get(self, key: string) -> Json? the value of key when self is an Object with that key
std-json.value.accessors.at-indexat pub fn at(self, index: usize) -> Json? the item at index when self is an Array and index is from 0 up to its length
std-json.value.accessors.textas_text pub fn as_text(self) -> string? the string of a Text
std-json.value.accessors.boolas_bool pub fn as_bool(self) -> bool? the value of a Bool
std-json.value.accessors.nullis_null pub fn is_null(self) -> bool true exactly for Null
std-json.value.accessors.i64as_i64 pub fn as_i64(self) -> i64? what Number.as_i64 gives for a Number
std-json.value.accessors.u64as_u64 pub fn as_u64(self) -> u64? what Number.as_u64 gives for a Number
std-json.value.accessors.f64as_f64 pub fn as_f64(self) -> f64? what Number.as_f64 gives for a Number
  1. std-json.value.accessorsThese methods are declared on Json in std.json. A negative index gives .None.
  2. std-json.value.accessors.integer-exactas_i64 and as_u64 give .None for a float, even one with no fraction, such as 1.0, as an integer type's from_json does.
Json as_i64() as_u64() as_f64()
-3 .Some(-3) .None .Some(-3.0)
18446744073709551615 .None .Some(18446744073709551615) .Some of the nearest f64, 2^64
2.0 .None .None .Some(2.0)
"2" .None .None .None
use std.json.Json

fn count(value: Json) -> i64:
    value.as_i64().unwrap_or(0)   # 0 for 2.0: a float is not an integer

Note. There is no accessor for the Number itself: match value: .Number(n) => ... gives it. The accessors take no default. Chain them with and_then (Option), or reach a nested value with pointer.

#JSON Pointer

pointer reaches a nested value in one call, by a JSON Pointer (RFC 6901):

Rule Method Result
std-json.pointerpointer pub fn pointer(self, path: string) -> Json? the value that path points to from self, or .None when there is none
  1. std-json.pointer.syntaxThe path "" points to self. Any other path starts with /, and each / begins one token, which runs to the next / or the end. A path that does not start with / gives .None.
  2. std-json.pointer.escapesIn a token, ~1 reads as / and ~0 as ~, from the left. Any other ~ stays as written, as in serde_json.
  3. std-json.pointer.stepEach token steps into the current value: an Object's member whose key is the token, or an Array's item. Any other value, or a key that is not there, gives .None.
  4. std-json.pointer.indexAn Array token is base-ten digits with no sign and no leading zero, and its value must be below the array's length. Any other token, - among them, gives .None.
pointer of {"db":{"pool":[5,6]},"a/b":1,"m~n":2,"":3} Result
"" .Some of the whole value
"/db/pool/1" .Some(6)
"/a~1b", "/m~0n" .Some(1), .Some(2)
"/" .Some(3): the key ""
"/db/pool/01", "/db/pool/2", "/db/pool/-" .None
"db" .None: no leading /
use std.json.{JsonError, parse}

fn pool_max(text: string) -> Result[i64, JsonError]:
    doc := parse(text)?
    .Ok(doc.pointer("/db/pool/max").and_then(fn(found): found.as_i64()).unwrap_or(10))

Why. RFC 6901 is the one path syntax JSON tools share, and serde_json and Go's JSON Pointer packages read it. A .None for any miss keeps one call for the common lookup, as get does.

#Json Objects

JsonObject is the type of an Object's members. Insertion order is part of its contract, so a config file parsed and written again keeps its keys where they were.

Rule Method Result
std-json.object.new-valuenew pub fn new() -> JsonObject an object with no members
std-json.object.lenlen pub fn len(self) -> usize the number of members; is_empty is len() == 0
std-json.object.getget pub fn get(self, key: string) -> Json? the value of key, or .None; contains_key is true for a key that has a member
std-json.object.set-refset pub fn set(&mut self, key: string, value: Json) -> void a new key becomes the last member, and an existing key keeps its place with the new value
std-json.object.remove-refremove pub fn remove(&mut self, key: string) -> Json? removes the member key and gives its value, or .None when there is no such key; the other members keep their order
std-json.object.keyskeys pub fn keys(self) -> List[string] the keys, in insertion order
  1. std-json.object.decl-structstd.json declares the struct JsonObject, with private fields and the methods above. JsonObject implements Iterable[(string, Json)], Eq, and Debug.
  2. std-json.object.orderkeys, iteration, and Debug give the members in insertion order.
  3. std-json.object.eqTwo JsonObjects are equal when they have the same keys with equal values, in any order.
  4. std-json.object.valueA JsonObject is a value. Copying it, or a Json that holds it, copies its members, so set and remove change only the place they are called on.
use std.json.{Json, JsonObject}

fn build() -> Json:
    let mut fields = JsonObject::new()
    fields.set("name", Json.Text("hd"))
    fields.set("id", Json.Null)
    fields.set("name", Json.Bool(true))   # keeps its place: keys() is ["name", "id"]
    Json.Object(fields)

#Numbers

A Number is a JSON number whose representation is private. It follows Rust's serde_json::Number: integers in the i64 and u64 ranges are exact, and every other number is an f64.

use std.json.Number

fn big() -> Number:
    Number::from_u64(18446744073709551615)

fn half() -> Number?:
    Number::from_f64(0.5)
  1. std-json.number.decl-structstd.json declares the struct Number, a value with no public member.
  2. std-json.number.reprA Number holds an integer below zero, an integer from zero up, or a finite f64. One integer has one representation, whichever constructor made it.
Rule Function Result
std-json.number.constructors.i64from_i64 pub fn from_i64(value: i64) -> Number the integer value
std-json.number.constructors.u64from_u64 pub fn from_u64(value: u64) -> Number the integer value
std-json.number.constructors.f64from_f64 pub fn from_f64(value: f64) -> Number? the float value, or .None for a NaN or an infinity
  1. std-json.number.constructorsThese three are static functions of Number, called as Number::from_i64(value).
Rule Method Result
std-json.number.accessors.i64as_i64 pub fn as_i64(self) -> i64? the integer when it is in the i64 range, and .None for a float
std-json.number.accessors.u64as_u64 pub fn as_u64(self) -> u64? the integer when it is from zero up, and .None for a negative integer or a float
std-json.number.accessors.f64as_f64 pub fn as_f64(self) -> f64? .Some of the float, or of the integer converted to the nearest f64
  1. std-json.number.accessorsThese three are methods of Number. as_f64 gives .Some for every Number, so no .None result means a number is out of range.

#Number Equality

  1. std-json.number.eqNumber implements Eq, with the rule of serde_json. Two numbers are equal when both are integers with the same value, or both are floats that are equal as f64 values.
  2. std-json.number.eq.mixedAn integer is never equal to a float, so 1 and 1.0 differ.
  3. std-json.number.eq.zeroThe floats 0.0 and -0.0 are equal, as f64 values are.
use std.json.Number

fn is_integer_two(n: Number) -> bool:
    n == Number::from_i64(2)   # false when n is the float 2.0

#Number Text

  1. std-json.number.displayNumber implements Display. An integer prints in base ten, as types.display.int gives, and a float prints as f64 does (types.display.float).
use std.json.Number

fn show(n: Number) -> string:
    "$n"   # an integer prints in base ten, a float as an f64

Note. The text of a finite float is always valid JSON. It has a decimal point or an exponent, as 1.0 and 1e+21, and negative zero is -0.0 (types.display.fixed).

Why. serde_json keeps three representations for the same reason. A u64 above i64::MAX and an i64 below zero must survive a round trip that an f64 would round. A JSON number is a decimal text, so a library that read every number as an f64 would lose the low bits of an identifier.

#Parsing

parse reads one JSON text:

use std.json.{parse, Json, JsonError}

fn title(text: string) -> string:
    match parse(text):
        .Ok(value) => value.get("title").and_then(fn(found): found.as_text()).unwrap_or("")
        .Err(error) => "$error"
Rule Function Result
std-json.parseparse pub fn parse(text: string) -> Result[Json, JsonError] .Ok of the value that text writes, or .Err of the first error

#Grammar

  1. std-json.parse.grammarparse accepts exactly the texts that the grammar of RFC 8259 gives for a JSON text, with any value at the top.
  2. std-json.parse.whitespaceWhitespace is a space, a tab, a line feed, or a carriage return. It may stand before and after the value, and between any two tokens.
  3. std-json.parse.strictThe grammar has no extension: no comment, no trailing comma, no single-quoted string, no NaN or Infinity, and no byte order mark.
  4. std-json.parse.endOnly whitespace may follow the value. The first other character is an UnexpectedCharacter error.
  5. std-json.parse.depthAn array or object nested inside 128 others is an error, and one nested inside 127 is not. The error is NestingTooDeep, at its opening bracket.
  6. std-json.parse.literalsThe words true, false, and null are lowercase only.
use std.json.parse

fn accepts(text: string) -> bool:
    match parse(text):
        .Ok(_) => true
        .Err(_) => false

fn checks() -> List[bool]:
    [accepts("[1, 2]"), accepts("[1, 2,]"), accepts("[1, /* c */ 2]")]
    # [true, false, false]: no trailing comma, no comment

#Objects

  1. std-json.parse.object.source-orderThe members of a parsed object are in the order of their first occurrence in the text.
  2. std-json.parse.object.duplicate-lastWhen a key occurs more than once, the object has one entry for it. Its value is the last one, and its place is the first one's.
  3. std-json.parse.object.keyA key is a string, and its escapes are decoded as a string's are.
use std.json.parse

fn last_wins() -> string:
    match parse("{\"b\": 1, \"a\": 2, \"b\": 3}"):
        .Ok(value) => "${value}"   # {"b":3,"a":2}: first place, last value
        .Err(_) => "error"

#Strings

  1. std-json.parse.string.escapesAn escape is a backslash and one of the following, and any other letter after a backslash is an InvalidEscape error.
Escape Value
\", \\, \/ the character after the backslash
\b, \f, \n, \r, \t U+0008, U+000C, U+000A, U+000D, U+0009
\u and four hex digits the UTF-16 code unit of those digits, in either case
  1. std-json.parse.string.hexA \u not followed by four hex digits is an InvalidEscape error at its backslash. When the text ends first, the error is UnexpectedEnd.
  2. std-json.parse.string.surrogatesA high surrogate escape, from \ud800 to \udbff, must be followed at once by a low surrogate escape, from \udc00 to \udfff. The pair is one scalar value.
  3. std-json.parse.string.lone-surrogateA surrogate escape with no partner is a LoneSurrogate error at the backslash of the first escape of the pair.
  4. std-json.parse.string.controlA raw character below U+0020 inside a string is a ControlCharacter error. A raw DEL (U+007F) and every other scalar value stand for themselves.
  5. std-json.parse.string.nul\u0000 is valid, and the string then holds U+0000.
use std.json.parse

fn decoded() -> string:
    match parse("\"a\\u0041\\n\""):
        .Ok(value) => value.as_text().unwrap_or("")   # "aA" and a line feed
        .Err(_) => ""

#Number Syntax

  1. std-json.parse.number.grammarA number is -, then 0 or a nonzero digit and digits, then an optional fraction, then an optional exponent. A character that breaks this is an UnexpectedCharacter error, or UnexpectedEnd at the end of the text.
  2. std-json.parse.number.integerAn integer in the i64 range or in the u64 range is exact, as Number.as_i64 or Number.as_u64 reads it.
  3. std-json.parse.number.negative-zero-0 is the float -0.0, as in serde_json, and 0 is the integer zero.
  4. std-json.parse.number.float-via-parseA number with a fraction or an exponent, or an integer outside both ranges, is the float that text.parse::[f64]() gives for its text.
  5. std-json.parse.number.nonfinite-parse-resultA float past the finite f64 range, which f64 parsing gives as an infinity, is a NumberOutOfRange error. The error sits at the number's first character, once its grammar is read to the end.
Text parse gives
"1.5" .Ok of the float 1.5
"18446744073709551616" .Ok of the float 2^64, one past the u64 range
"1e-400" .Ok of the float 0.0
"1e400" .Err(NumberOutOfRange(0))

Why. serde_json reads numbers the same way. An integer in range is exact, and every other number is the nearest f64. A number past the f64 range is an error, since a Number holds no infinity (std-json.number.repr).

use std.json.parse

fn read_number(text: string) -> string:
    match parse(text):
        .Ok(.Number(n)) => "$n"
        _ => "not a number"

fn demos() -> List[string]:
    [read_number("1.5"), read_number("-0"), read_number("18446744073709551616")]
    # ["1.5", "-0.0", "18446744073709552000.0"]

#Error Order

  1. std-json.parse.error-orderparse reads the text from the left. Its error is the first position at which no continuation is valid, with the kind that the position gives.
Text parse gives
"" .Err(UnexpectedEnd(0))
"[1,]" .Err(UnexpectedCharacter(3))
"01" .Err(UnexpectedCharacter(1))
"\"\\ud800\"" .Err(LoneSurrogate(1))
"1e400" .Err(NumberOutOfRange(0))
"1.x" .Err(UnexpectedCharacter(2))
use std.json.parse

fn first_error(text: string) -> string:
    match parse(text):
        .Ok(_) => "ok"
        .Err(error) => "$error"

fn demo() -> string:
    first_error("[1,]")   # unexpected character at position 3

#JSON Errors

A failed parse reports one of seven errors, each at a position. A failed decode reports a parse error, or one of three errors at a path:

pub enum JsonError:
    UnexpectedEnd(position: usize)
    UnexpectedCharacter(position: usize)
    InvalidEscape(position: usize)
    LoneSurrogate(position: usize)
    ControlCharacter(position: usize)
    NumberOutOfRange(position: usize)
    NestingTooDeep(position: usize)
    WrongType(path: string, expected: string)
    MissingField(path: string)
    UnknownVariant(path: string, name: string)
  1. std-json.error.variantsstd.json declares the enum JsonError with the ten variants above. The first seven are parse errors, and the last three are decode errors.
  2. std-json.error.position-bytesA position is a byte offset into the text, counted in bytes of its UTF-8 encoding from 0, as slice counts.
  3. std-json.error.traitsJsonError implements Eq, Debug, and Display.
Rule Variant Position
std-json.error.kind.endUnexpectedEnd the text stops before the value is complete the length of the text
std-json.error.kind.characterUnexpectedCharacter a character that the grammar does not allow there that character
std-json.error.kind.escapeInvalidEscape a backslash escape that is not in the table, or a \u without four hex digits the backslash
std-json.error.kind.surrogateLoneSurrogate a surrogate escape with no partner the backslash of the first escape
std-json.error.kind.controlControlCharacter a raw character below U+0020 in a string that character
std-json.error.kind.rangeNumberOutOfRange a number past the finite f64 range its first character
std-json.error.kind.depthNestingTooDeep an array or object past 128 levels its opening bracket

Note. A position is a valid slice bound, so text.slice(0, position) is the text before the error, even after a non-ASCII character.

#Decode Errors

A decode error names the value that failed by its path from the root, as $.users[2].name:

Rule When
std-json.error.kind.typeWrongType a value of another kind than the type reads, or a number outside the type's range; expected names what the type reads
std-json.error.kind.missingMissingField an object without the key of a member whose type does not read null; path ends with that key
std-json.error.kind.variantUnknownVariant a variant name that the enum does not declare; path is the enum value's, and name is the name read
  1. std-json.error.pathA path is $, then one segment per step from the root value. A segment is .key for an object's member or a variant's payload, and [index] for an array item.
  2. std-json.error.path.indexAn index is written in base ten, counted from 0.
  3. std-json.error.path.keyA key is written as it is, with no quotes and no escapes.
  4. std-json.error.display.pathThe Display text of a decode error contains its path.

The expected of a WrongType depends on the type that reads the value:

Rule Type expected
std-json.error.expected.scalarScalars bool, char, string, and each integer and float type the type's name, as "u8"
std-json.error.expected.arrayLists List[T] "array"
std-json.error.expected.objectObjects Map[string, V], a derived struct, and a derived variant's payload "object"
std-json.error.expected.variantEnums a derived enum "variant"

Note. A path is for people to read. A key that holds . or [ makes it ambiguous, as in serde_path_to_error.

use std.json.{decode, JsonError}
use std.serde.{Serialize, Deserialize}

@derive(Serialize, Deserialize)
struct User:
    name: string
    age: i32

fn load(text: string) -> Result[User, JsonError]:
    decode::[User](text)

fn report(text: string) -> string:
    match load(text):
        .Ok(user) => user.name
        .Err(error) => "$error"   # "{\"name\": \"ada\"}" misses $.age

#Writing

A Json writes as JSON text in two layouts, compact and pretty:

use std.json.{parse, Json}

fn show(value: Json) -> string:
    "${value}"   # {"a":[1,2]}

fn show_pretty(value: Json) -> string:
    value.pretty()
Rule Method Result
std-json.value.prettypretty pub fn pretty(self) -> string the pretty text of self

Why. Text output of a value is a method, as to_rfc3339() and to_fixed(n) are. One spelling is enough, so there is no free pretty function.

#Compact Text

  1. std-json.display.compactJson implements Display. Its text has no whitespace outside strings: [1,2] and {"a":1}.
  2. std-json.display.kindsNull prints as null, a Bool as true or false, and a Number as std-json.number.display gives.
  3. std-json.display.containersAn Array prints its items in order, and an Object prints its members as "key":value, each list separated by ,.
  4. std-json.display.object-orderAn Object prints its members in insertion order, in compact text and in pretty text. Printing a parsed object gives its keys in the text's order.
  5. std-json.display.textA Text and each key print in quotes. A quote, a backslash, and each character below U+0020 are escaped, and every other scalar value prints as itself.
  6. std-json.display.escapesThe escapes are \", \\, \b, \f, \n, \r, and \t, and \u00 with two lowercase hex digits for the other controls. / is not escaped.
use std.json.{Json, parse}

fn compact(value: Json) -> string:
    "$value"   # no whitespace outside strings: [1,2] and {"a":1}

fn demo() -> string:
    match parse("{ \"a\" : [ 1, 2 ] }"):
        .Ok(value) => compact(value)   # {"a":[1,2]}
        .Err(_) => "error"

Note. The compact text of a value that parse read parses back to an equal value.

#Pretty Text

  1. std-json.pretty.layoutpretty writes the compact text's tokens with each item of an array and each entry of an object on its own line. Each line is indented by two spaces for each level of nesting.
  2. std-json.pretty.separatorAn entry is "key": value, with one space after the colon. A comma follows every item or entry but the last, with no space before it.
  3. std-json.pretty.emptyAn empty array prints as [] and an empty object as {}, on one line.
  4. std-json.pretty.scalarA value that is not an array or an object prints as its compact text, with no line break.
  5. std-json.pretty.no-trailingThe text ends with its last bracket, brace, or scalar: no trailing space, no final line break.
Json pretty text
[1,2] "[\n 1,\n 2\n]"
{"a":[]} "{\n \"a\": []\n}"
use std.json.parse

fn pretty_demo(text: string) -> string:
    match parse(text):
        .Ok(value) => value.pretty()
        .Err(_) => ""

#Typed JSON

JSON is a format that reads a type's serialization opt-in. to_json writes any value that implements Serialize as a Json, and from_json builds a Deserialize value from one. encode and decode join them to JSON text:

use std.json.{encode, decode, JsonError}
use std.serde.{Serialize, Deserialize}

@derive(Serialize, Deserialize)
struct Session:
    pub user: string
    token: string
    expires_at: i64

fn save(session: Session) -> string:
    encode(session)   # {"user":"ada","token":"t-81f2","expires_at":1700000000}

fn load(text: string) -> Result[Session, JsonError]:
    decode::[Session](text)

#Encode And Decode

Rule Function Result
std-json.to-jsonto_json pub fn to_json[T < Serialize](value: T) -> Json the Json that value's serialize writes
std-json.from-jsonfrom_json pub fn from_json[T < Deserialize](value: Json) -> Result[T, JsonError] the T that T::deserialize reads from value, or the first error it finds
std-json.encodeencode pub fn encode[T < Serialize](value: T) -> string the compact text of to_json(value)
std-json.encode-prettyencode_pretty pub fn encode_pretty[T < Serialize](value: T) -> string the pretty text of to_json(value), as to_json(value).pretty() gives
std-json.decode.directdecode pub fn decode[T < Deserialize](text: string) -> Result[T, JsonError] the T that T::deserialize reads directly from validated JSON text
  1. std-json.decode.parse-errorWhen parse fails, decode returns its parse error and reads no value.
  2. std-json.from-json.errorsfrom_json returns only the decode errors: WrongType, MissingField, and UnknownVariant.
  3. std-json.from-json.rootEach path that from_json reports starts at the value it reads, whose path is $.
  4. std-json.encode.serializeencode or to_json of a type that does not implement Serialize is an error. Error: unsatisfied-trait-bound.
  5. std-json.decode.deserializedecode or from_json into a type that does not implement Deserialize is an error. Error: unsatisfied-trait-bound.
  6. std-json.decode.no-treedecode reads typed values without constructing an intermediate Json tree.
Error example
use std.json.{encode, decode, JsonError}

struct Plain:
    id: i32

fn show(value: Plain) -> string:
    encode(value)  # error: unsatisfied-trait-bound

fn load(text: string) -> Result[Plain, JsonError]:
    decode::[Plain](text)  # error: unsatisfied-trait-bound

Why. JSON declares no trait of its own, so a type's one opt-in serves JSON and every other format alike, as serde_json reads serde's Serialize.

use std.json.encode_pretty
use std.serde.Serialize

@derive(Serialize)
struct Point:
    x: i32
    y: i32

fn show(point: Point) -> string:
    encode_pretty(point)   # "{\n  \"x\": 3,\n  \"y\": 4\n}" for Point { x: 3, y: 4 }

Note. encode_pretty differs from encode only in whitespace outside strings: the keys, their order, and the escapes are the same.

#Standard Implementations

JSON writes and reads each standard implementation of Serde as below:

Rule Type to_json gives from_json reads
std-json.std.jsonJson Json the value itself any value, as itself
std-json.std.boolBoolean bool a Bool a Bool
std-json.std.textString string a Text a Text
std-json.std.charCharacter char a Text of that one character a Text of exactly one character
std-json.std.integerIntegers i8, i16, i32, i64, u8, u16, u32, u64, and usize the exact integer, as Number::from_i64 or Number::from_u64 gives it a Number that is an integer in the type's range
std-json.std.floatFloats f32 and f64 the Number of the value as an f64 any Number, as as_f64 gives it
std-json.std.optionalOptional T?, where T implements the trait null for .None, and the payload's Json for .Some .None from null, and .Some of what T reads from any other value
std-json.std.listList List[T], where T implements the trait an Array of the items' values, in order an Array, item by item
std-json.std.mapMap Map[string, V], where V implements the trait an Object of the entries' values, in the map's iteration order an Object, entry by entry, in its key order
  1. std-json.std.json.implsstd.json implements Serialize and Deserialize for Json. A Json reads through peek, so it takes any value.
  2. std-json.std.integer.readA float is never an integer, so 1.0 is a WrongType for every integer type. So is an integer outside the type's range.
  3. std-json.std.float-specialA NaN or an infinity has no JSON number, so its to_json is null, as in serde_json.
  4. std-json.std.float-readAn f32 reads the nearest f32 to the number's f64. A number past the finite f32 range is a WrongType.
  5. std-json.std.wrong-kindAny other kind of value is a WrongType, with the expected that Decode Errors gives.
  6. std-json.std.item-pathAn error inside a list item gets the segment [index], and one inside a map value gets .key.
use std.json.{encode, decode, JsonError}

fn round_trip(names: List[string]) -> Result[List[string], JsonError]:
    decode::[List[string]](encode(names))

Note. T?? loses a layer: .Some(.None) writes null, which reads back as .None. A NaN writes null, which no float reads.

Note. Only a Map with string keys implements the trait, by std-serde.std.map-keys, so encode of a Map[i32, i32] is an unsatisfied-trait-bound error.

#Derived Serialize

JSON writes a struct whose Serialize is derived as an object with one key per member. It writes an enum in the externally tagged form, the variant's name with its members:

use std.json.encode
use std.serde.{Serialize, Deserialize}

@derive(Serialize, Deserialize)
enum Shape:
    Circle(radius: u32)
    Rect(width: i32, height: i32)
    Point(i32, i32)
    Empty
Value encode text
Shape.Circle(7) {"Circle":{"radius":7}}
Shape.Rect(2, 3) {"Rect":{"width":2,"height":3}}
Shape.Point(4, 5) {"Point":{"_0":4,"_1":5}}
Shape.Empty "Empty"
  1. std-json.derive.to-json.data-included-membersA struct gives an Object with one key per included member that the derivation walks, in declaration order. The key is its wire name, and its value is the member's Json.
  2. std-json.derive.to-json.enum-included-membersA variant with included members gives an Object with one key, the variant's name. Its value is the Object of the variant's members, as for a struct.
  3. std-json.derive.to-json.unit-included-membersA variant with no included member for this value gives the Text of its name.
  4. std-json.derive.to-json.positional-wire-nameA positional member's key is its wire name, defaulting to _0, _1, and so on.
  5. std-json.derive.to-json.embedded-wire-nameAn embedded member is one key using its wire name, defaulting to its type's final name, with the part's Json as value.
  6. std-json.derive.to-json.sharedShared constructor data is not a member, so it is not written.
  7. std-json.derive.to-json.privateA private member is written like any other, since the opt-in covers it.

Note. A member omitted with = pass in a derivation block writes no key, as Omitted Members states. Renames and conditional skips are open, in Serialization Formats.

Why. The externally tagged form is serde's default. It needs no tag key that could clash with a member's name.

#Derived Deserialize

JSON reads the form that it writes for a derived Serialize:

Text read as a Shape Result
"Empty" or {"Empty":{}} .Ok(Shape.Empty)
{"Rect":{"height":3,"width":2,"z":0}} .Ok(Shape.Rect(2, 3))
"Rect" .Err(MissingField("$.Rect.width"))
"Hexagon" .Err(UnknownVariant("$", "Hexagon"))
{"Rect":3} .Err(WrongType("$.Rect", "object"))
{"Rect":{},"Empty":{}} .Err(WrongType("$", "variant"))
  1. std-json.derive.from-json.inverseA derived Deserialize reads the form that a derived Serialize writes for the same type.
  2. std-json.derive.from-json.dataA struct reads an Object. Any other value is a WrongType.
  3. std-json.derive.from-json.member-wire-nameIt reads each built member in declaration order from its wire key, ending on the first error with that wire name's path segment.
  4. std-json.derive.from-json.unknownA key that names no member is ignored.
  5. std-json.derive.from-json.missingA missing key reads as null: the member's type reads Json.Null. When that read fails, the error is a MissingField at the path of the missing key.
  6. std-json.derive.from-json.defaultA missing key of a member that declares a default gives that default, and the member is not read, so it is never a MissingField. The serde reader reports the key absent through is_absent.
  7. std-json.derive.from-json.null-presentA key whose value is null is not missing: the member reads Json.Null even when it declares a default, so a T? member gets .None.
  8. std-json.derive.from-json.enumAn enum reads a Text, or an Object with exactly one key. The text or the key is the variant's name. Any other value is a WrongType.
  9. std-json.derive.from-json.name-onlyA Text reads as an Object whose one key is that text and whose value is an empty Object.
  10. std-json.derive.from-json.unknown-variantA name that is not a variant of the enum is an UnknownVariant at the enum value's path.
  11. std-json.derive.from-json.payloadThe variant's payload must be an Object, or it is a WrongType with the segment .name of the variant. Its members then read as a struct's do, under that segment.
use std.json.{decode, JsonError}
use std.serde.{Serialize, Deserialize}

@derive(Serialize, Deserialize)
enum Shape:
    Circle(radius: u32)
    Rect(width: i32, height: i32)
    Point(i32, i32)
    Empty

fn read(text: string) -> Result[Shape, JsonError]:
    decode::[Shape](text)

fn shape_of(text: string) -> string:
    match read(text):
        .Ok(Shape.Empty) => "empty"
        .Ok(_) => "a shape with members"
        .Err(error) => "$error"

A missing key takes the member's declared default first. With no default, a T? member reads .None, and any other member is a MissingField:

Text read as a Server Result
{"host":"db1"} .Ok(Server { host: "db1", port: 8080, banner: .Some("hi"), note: .None })
{"host":"db1","port":5432,"banner":null} port 5432, banner .None
{"port":5432} .Err(MissingField("$.host"))
{"host":"db1","port":null} .Err(WrongType("$.port", "u16"))
use std.json.{decode, JsonError}
use std.serde.Deserialize

@derive(Deserialize)
struct Server:
    host: string
    port: u16 = 8080
    banner: string? = .Some("hi")
    note: string?

fn load(text: string) -> Result[Server, JsonError]:
    decode::[Server](text)   # {"host":"db1"} gives port 8080

Note. So a member of type T? may be missing, and reads .None unless it declares a default, and a member of type Json reads null. A member of any other standard type must be present or declare a default.

Why. serde ignores unknown keys by default, so a reader accepts output from a newer writer. A missing optional reads as absent, as serde's Option does. A missing key with a declared default takes it, as serde's #[serde(default)] and Go's json.Unmarshal into a filled struct do, so a config file may leave out what the type already settles. A null is a value the text wrote, so it is read.

See also: Encoding, Num, Collections, Numeric Display, Map Lookup And Order, Serialization, Serde, Derived Hashing.