Issue #604: json as an official package: read[T]/write[T] by ADR-299’s rules, a build-time schema, json::Value (ADR-344)

Open and answer this issue on GitHub.

libraries, ready, wip · opened 2026-10-13 · updated 2026-11-03 · 5 comments

History

ADR-338 (#539) made JSON an official package that reads and writes, but left open where it lives, what a read returns and what is checked when. The concrete need: reading JSON of a shape no struct declares up front, such as LSIF (#579).

Checked with target/release/nikaia run: a loop over T::fields unrolls per type, and .name, .of(value), .attribute(X) work. .ty, a type as a parameter (schema(CreateUser)) and T::kind do not exist yet (field.ty is NK1180, a type parameter is a parse error): ADR-329 and ADR-330 specify them but nothing is built, tracked in #859. So write[T], the schema and read[T] wait for #859. (An earlier version of this text said write[T] could be written now; that was wrong.) Building a value from reflected fields is not specified by either ADR and is a separate open decision. A String? field in an unrolled println reaches rustc as a Display error instead of a Nikaia diagnostic.

Owner’s answers of 2026-10-13: packages in packages/<name>/, own version only; first a plain package by path dependency, no dsl export, no ADR-338 D3; json::read[T](text) as a call with a build-time decoder and a thrown error with position; mapping by ADR-299 D16-D18, unknown members refused unless the type allows it; a JSON Schema as a build-time input, run-time constraints not decided; json::write[T].

Owner’s answers of 2026-10-26 (ADR-344 D6a): the dynamic form json::Value / json::parse stays, without warning for now (a deprecated attribute may follow once the static form is shown to cover every use). Numbers in Value stay text (Number(String)) and are converted when asked for as a type (as_i64, as_u64, as_f64). The static form is exactly ADR-344 D3: json::read[T](text) returns a value of a user-defined struct T.

Decision

ADR-344, Part III 17.3 (json), codes NK1250 (field type JSON cannot be) and NK1251 (T does not match the schema) in C.3.

Proposals the owner has not yet confirmed (ADR-344 D4): the opt-in for unknown members as the attribute @json::Open (and @json::Rest on a Map[String, json::Value] field to keep them); whether a missing member goes into a T? as null by default.

To build

  1. Done (packages/json/): enum Value with Number(String), parse(text) -> Value throws Error with line, column, path, as_i64/as_u64/as_f64.
  2. Waits for #859 (.ty, type parameter, T::kind): json::write[T] and write of a Value over T::fields with @json::Name. Tests: round trip parse(write(v)) == v; field order; null for an empty T?; escaping.
  3. Waits for #859 and for a decision on building a value from reflected fields (spec text first, Part II 10.3): json::read[T]. Tests: by name in any order; a duplicate member, a missing field, an unknown member, null into a non-T? each throw with path; @json::Open; nested structs, Vec, Map; NK1250 on a field of a function type.
  4. Waits for #859: schema: read through asset, T checked at build time. Tests: one .nika per NK1251 case (missing required, extra field, type mismatch, nullable into plain), and a passing case with additionalProperties.
  5. ADR-338 D3 (a use json takes the package in) and the grammar export (#520, #485), see #539.

Generated by Claude Code

Comments

keywan-ghadami, 2026-10-13

Owner’s answers of 2026-10-13 (second round), in ccd3993 (0.0.882)

  1. Missing members: a field with no member takes its default if it has one (Part I field defaults), is null in a T?, and is otherwise an error where the text is read. Replaces the strict “missing is an error as in ADR-299 D17” for T?. This is a run-time throw, so no NK code; the compile-time case (a schema requires a property T lacks) stays NK1251.
  2. Unknown members: not decided. The specification keeps ADR-299 D17’s strict rule (error). @json::Open / @json::Rest are removed from Part III 17.3 and stand in ADR-344 D4 as proposal, not decided.
  3. Errors: json::Error has the form of a grammar’s ParseError (ADR-001 D2, Part III Appendix C): line, column and the stack of what was being read; the path $.a[3].b is that stack as JSON names it, not a second report.
  4. json::parse does not exist; removed.
  5. json::Value put back as a whole (out of Part III, ADR-344 “Not decided here”), with the owner’s direction: numbers kept as text/a view and converted on access (writers put 12.0 for integers); member order put back with it. Old D8 is now D7.

NK1250 (field type JSON cannot be; json::Value no longer in the list) and NK1251 still hold as written in C.3.

To build (replaces the list above)

  1. packages/json/ (nikaia.toml, own version 0.1.0): Error in ParseError’s form (line, column, rule stack with path). Tests: an error position and path on a truncated text and on a bad value.
  2. json::write[T] over T::fields with @json::Name. Tests: field order; null for an empty T?; escaping; nested struct, Vec, Map.
  3. Building a value from reflected fields and ADR-330’s type parameter in the compiler (spec text for the construction first, Part II 10.3), then json::read[T]. Tests: by name in any order; round trip read[T](write(v)) == v; a missing T? member is null; a missing member with a default takes the default; a missing required member, a duplicate member, an unknown member, null into a non-T? each throw with path; escapes and surrogate pairs; NK1250 on a field of a function type.
  4. schema: read through asset, T checked at build time. Tests: one .nika per NK1251 case (missing required, extra field, type mismatch, nullable into plain), and a passing case with additionalProperties.
  5. ADR-338 D3 (a use json takes the package in) and the grammar export (#520, #485), see #539.

Generated by Claude Code

keywan-ghadami, 2026-10-13

Owner decision, 2026-10-13: unknown members are skipped. (commit 7d37ef5, version 0.0.884)

How it came up: ADR-344 D4 had left open whether json::read[T] may accept a member T has no field for. Until then ADR-299 D17’s rule applied: such a member was an error. There was also a proposal, @json::Open, to skip unknown members on a single type.

Decision: a member the target type has no field for is ignored. It is not an error. The owner’s reason: in practice these members are protocol extensions, and those must not cause failures. This departs from ADR-299 D17 on purpose. A SQL result has exactly the columns the query names. JSON from outside grows without the program doing anything. With this rule, @json::Open is no longer needed.

To build (addition):


Generated by Claude Code

keywan-ghadami, 2026-10-13

Owner’s decision of 2026-10-13: an explicit null for a field that is not a T? takes the field’s default if it has one; without a default it stays an error. Recorded in ADR-344 D4/D8 (reason; rejected: “null only into a T?”) and Part III 17.3, version 0.0.885 (50d7533).

Test to add to the To build list:


Generated by Claude Code

keywan-ghadami, 2026-10-25

Step 1 is built and on develop (151f9fd, plus 569cfb9 for nikaia.lock and the .gitignore entry): packages/json 0.1.0 with Value, Member, parse(text) -> Value throws Error (message, line, column, path like $.a.b[2].c). Five Nikaia tests in packages/json/tests/.

Decided while building (differs from ADR-344 text):

Compiler fixes in the same commit (the package tests are their regression tests):

Found, not fixed (to file as ready):

  1. No cast from i32/u32/i64 to scalar (only u8 as scalar works).
  2. A struct used as an error cannot be matched by its bare name: match error { E => ... } lowers to invalid Rust.

Still to build: steps 2-6. #850 can move agent_json to json::parse now.


Generated by Claude Code

keywan-ghadami, 2026-10-30

Update (0.0.1314): ADR-grammar-2 is accepted. json::read[T] is now a forwarding function to the entry Json::read[T] of the package’s grammar. The compiler emits that entry once per T and builds the struct with members[S] (ADR-344 D3 and D6a changed).

The open decision “building a value from reflected fields” (step 3) is no longer needed: the compiler builds the value. field.set stays NK1180.

What each step of the list above becomes:

Also decided:


Generated by Claude Code

Back to Open Issues