#gaato/mttp/jsonkit

    Helpers for hand-written and generated JSON models over moonbitlang/core's Json, ToJson and FromJson:

    • Presence[T] keeps the three states of an optional property apart: omitted (Absent), null (Null) and a value (Value). An API that treats "leave unchanged" and "clear" differently needs all three.
    • ObjBuilder writes an object field by field: field always, opt only for Some, presence omitting Absent and writing null for Null. Fields keep the position of their first write.
    • field, opt_field, presence_field and expect_object decode an object's fields with the key added to the error path.
    • open_string and open_int decode an enum that may grow on the server, keeping values this client does not know instead of failing.

    The package is named jsonkit, not json, so its alias does not shadow core's @json.

    The decoders take the JsonPath that core hands a FromJson implementation, so errors name the failing field:

    ///|
    #derive(Eq, Debug)
    struct Item {
    id : Int32
    note : @jsonkit.Presence[String]
    state : String
    }

    ///|
    impl FromJson for Item with fn from_json(json, path) {
    let obj = @jsonkit.expect_object(json, path)
    {
    id: @jsonkit.field(obj, "id", path),
    note: @jsonkit.presence_field(obj, "note", path),
    state: @jsonkit.open_string(
    obj.get("state").unwrap_or(Json::null()),
    path.add_key("state"),
    s => if s == "open" { Some(s) } else { None },
    s => "unknown:" + s,
    ),
    }
    }

    ///|
    impl ToJson for Item with fn to_json(self) {
    @jsonkit.ObjBuilder::new()
    .field("id", self.id)
    .presence("note", self.note)
    .field("state", self.state)
    .build()
    }

    ///|
    test "a model with a required, a nullable and an open field" {
    let item : Item = @json.from_json({
    "id": 7,
    "note": null,
    "state": "archived",
    })
    assert_eq(item, { id: 7, note: Null, state: "unknown:archived", })
    assert_eq(ToJson::to_json(item), {
    "id": 7,
    "note": null,
    "state": "unknown:archived",
    })
    let missing = try (@json.from_json({ "note": "x" }) : Item) catch {
    @json.JsonDecodeError((path, message)) => "\{path}: \{message}"
    } noraise {
    _ => "decoded"
    }
    assert_eq(missing, "/id: missing required field")
    }

    ObjBuilder

    pub struct ObjBuilder {
    // private fields
    }

    Mutable object builder retaining each key's original insertion position.

    test {
    let builder = @jsonkit.ObjBuilder::new()
    .field("name", "sample")
    .opt("count", (None : Int32?))
    .presence("note", (@jsonkit.Null : @jsonkit.Presence[String]))
    assert_eq(builder.build(), { "name": "sample", "note": null })
    }

    ObjBuilder::build

    Returns a snapshot of the current fields, preserving insertion order.

    ObjBuilder::field

    fn[T :
    ToJson
    ] ObjBuilder::field(self : ObjBuilder, key : String, value : T) -> ObjBuilder

    Writes a field, replacing any previous value at its original position.

    ObjBuilder::new

    fn ObjBuilder::new() -> ObjBuilder

    Creates an empty builder.

    ObjBuilder::opt

    fn[T :
    ToJson
    ] ObjBuilder::opt(self : ObjBuilder, key : String, value : T?) -> ObjBuilder

    Writes Some values and leaves the builder unchanged for None.

    ObjBuilder::presence

    fn[T :
    ToJson
    ] ObjBuilder::presence(self : ObjBuilder, key : String, value : Presence[T]) -> ObjBuilder

    Omits Absent, writes null for Null, and encodes Value.

    Presence

    pub(all) enum Presence[T] {
    Absent
    Null
    Value(T)
    } derive(Eq,
    Debug
    )

    Distinguishes an omitted property from null and a present value.

    test {
    let value = @jsonkit.Presence::from_option(Some(3))
    assert_eq(value.map(n => n + 1).to_option(), Some(4))
    assert_false(value.is_absent())
    }

    Presence::equal

    fn[T : Eq] Presence::equal(Presence[T], Presence[T]) -> Bool

    Compares presence and, for Value, the contained value.

    Presence::from_option

    fn[T] Presence::from_option(value : T?) -> Presence[T]

    Represents None as omission.

    Presence::is_absent

    fn[T] Presence::is_absent(self : Presence[T]) -> Bool

    Reports omission, as distinct from an explicit null.

    Presence::map

    fn[T, U] Presence::map(self : Presence[T], f : (T) -> U) -> Presence[U]

    Transforms only a present value, preserving omission and null.

    Presence::not_equal

    fn[T : Eq] Presence::not_equal(x : Presence[T], y : Presence[T]) -> Bool

    Compares presence and, for Value, the contained value.

    Presence::to_option

    fn[T] Presence::to_option(self : Presence[T]) -> T?

    Collapses both omission and null to None.

    Presence::to_repr

    Formats the presence state and its value for debugging.

    expect_object

    Extracts an object's fields or raises at the supplied path.

    field

    Decodes a required field, attaching the key to missing or invalid value errors.

    open_int

    fn[T] open_int(json :
    Json
    , path :
    JsonPath
    , known : (Int) -> T?, unknown : (Int) -> T) -> T raise
    JsonDecodeError

    Decodes an open integer enum, rejecting fractions and values outside Int.

    open_string

    fn[T] open_string(json :
    Json
    , path :
    JsonPath
    , known : (String) -> T?, unknown : (String) -> T) -> T raise
    JsonDecodeError

    Decodes a string enum, preserving unrecognized values through unknown.

    opt_field

    Treats missing fields and null as None; other values must decode as T.

    presence_field

    Preserves missing, null, and decoded values as three distinct cases.