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
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.
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.
packages/json/): enum Value with Number(String), parse(text) -> Value throws Error with line, column, path, as_i64/as_u64/as_f64..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.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.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.use json takes the package in) and the grammar export (#520, #485), see #539.Generated by Claude Code
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.@json::Open / @json::Rest are removed from Part III 17.3 and stand in ADR-344 D4 as proposal, not decided.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.json::parse does not exist; removed.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.
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.json::write[T] over T::fields with @json::Name. Tests: field order; null for an empty T?; escaping; nested struct, Vec, Map.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.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.use json takes the package in) and the grammar export (#520, #485), see #539.Generated by Claude Code
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.
json paragraph.@json::Rest, because it needs the dynamic form, which is itself deferred. An opt-in strict mode, because no program needs one yet.To build (addition):
{"name":"a","email":null,"extra":{"x":[1,2]}} read with json::read[Contact] into struct Contact { name: String, email: String? } gives Contact { name: "a", email: null } and no error. A malformed value inside the extra member is still a json::Error.Generated by Claude Code
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:
{"n": null} read into a struct with n: i64 = 5 gives n == 5; the same into n: i64 (no default) is an error with the member’s position.Generated by Claude Code
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):
json::parse and numbers stay as text. The package has parse and splits numbers: a whole number is Int, while 1.0, 1e3 and a whole number outside i64 are Float. The ADR text needs updating to match.ParseError carries no line, column or path.Compiler fixes in the same commit (the package tests are their regression tests):
lib::V); before, NK1102.lib::Fault) and catch can read their fields; before, NK1245 and a rustc error.Found, not fixed (to file as ready):
i32/u32/i64 to scalar (only u8 as scalar works).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
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:
write[T], read[T], schema) are now #1019 steps 7, 1-6 and 9.Also decided:
json::Value as a field, time::Instant, @json::Rest) is NK1250 until decided (D12, #1020);@json::Open/@json::Rest as proposed by ADR-344 D4 wait for that decision.Generated by Claude Code