json::Value as a field, time::Instant, @json::Rest)Open and answer this issue on GitHub.
language, ready · opened 2026-10-30 · updated 2026-11-03 · 3 comments
Since ADR-grammar-2 (0.0.1314), a grammar entry can take a type, Json::read[Contact](bytes), and the compiler emits one parser for that type. A match on the field’s kind chooses the pattern: Struct(_), List(A), Maybe(A), Text, Map(K, V) and so on.
struct Event { name: String, at: time::Instant, extra: json::Value }
let e = json::read[Event](bytes) // today: NK1250 on `at` and on `extra`
Some fields are not described by their kind but by their type:
time::Instant is a struct, but in JSON it is a text such as "2026-10-30T12:00:00Z";json::Value field means “keep this part of the input as a tree”;@json::Rest on a Map[String, json::Value] field means “collect the members no other field takes” (ADR-344 D4, proposed, not confirmed).A kind arm cannot express any of these. ADR-grammar-2 D12 refuses such fields with NK1250 until this is decided.
ADR-grammar-2 §6, from the architect’s proposal of 2026-10-30. Nothing needs it now: the first slice of #1019 works without it. It becomes needed when a program reads JSON with timestamps or with a part of unknown shape. Both are common, so it is a basic question for the format packages, not bookkeeping.
NK1250. A program reads the text into a String and converts by hand. For: nothing new. Against: every timestamp needs hand code; @json::Rest is impossible.json::Value => Any, time::Instant => s:Str -> { time::parse_rfc3339(s)? }. For: the format decides, written where the other arms are; one place. Against: the format package must know every such type; a user’s own type with a text form cannot join without changing the package.T: json::FromText => s:Str -> { T::from_text(s)? }, where a type opts in by implementing a trait of the format. For: user types can join; the format stays closed. Against: a trait per format, implemented per type (serde’s cost on a smaller scale); the order of bound arms against kind arms must be specified.json::Value), a bound for everyone else. For: covers both. Against: two forms to learn.@json::With(parse_rfc3339), naming a function. For: local, no trait. Against: repeated on every field of that type; the grammar has to call a user function from an action.| Language / tool | Answer |
|---|---|
| Rust, serde | the type implements Deserialize itself (C), or #[serde(with = "…")] on the field (E); #[serde(flatten)] for the rest (Rest) |
Zig std.json |
a type may declare jsonParse, checked with @hasDecl (C without a trait) |
| Go | the type implements json.Unmarshaler (C); json.RawMessage keeps a raw part (B, for the format’s own type) |
| Kotlin serialization | @Serializable(with = …) on the type or the field (C/E); JsonElement keeps a tree (B) |
Every one of these has both: the format’s own types handled by name, and an opt-in for others.
D: an exact-type arm for the format’s own types (json::Value, @json::Rest’s map), and a bound arm for every other type, with a trait per format. Exact-type arms are tried before bound arms, and bound arms before kind arms. This matches what Go, Kotlin and Zig converged on, keeps the format package closed, and lets time::Instant opt in from std’s side.
Blocks: nothing now. @json::Rest and json::Value fields in #1019 wait for it.
The owner worked this out in conversation. The result goes beyond options A to E: it is option C, with the trait carrying a rule instead of a function.
std provides FromText and FromNumber. The list of kinds is open, and a format package may add its own traits (json::RawText, toml::LocalDate). There is no new kind keyword, because T::kind already means meta::Type (ADR-330).trait FromText { rule from_text -> Self }
The trait decides what the rule reads. FromText reads decoded text, so escapes are already applied. A raw trait of a format reads the characters undecoded, which is how a high-performance type can skip escape decoding. The compiler may still choose to read raw when the type’s grammar accepts no backslash.
grammar block around it:
impl FromText for Zeitpunkt {
rule from_text -> Zeitpunkt =
d:Datum "T" u:Uhrzeit "Z" ->
{ return Zeitpunkt::new(d, u) }
}
return.rule in an impl is shorthand for a grammar with exactly this one rule as its entry. It uses the same mechanism and lowering as grammar.rule from_text -> Zeitpunkt = Iso8601::zeitpunkt.d:Datum inside a FromText rule needs Datum: FromText. If that impl is missing, the error names exactly that missing impl.Zeitpunkt::from_text(s) runs the rule over the whole input and throws when it does not match. This works the same way as calling a grammar entry today (Part II 10.2).Self.FromText and FromNumber.Datum, Uhrzeit) becomes its own type with its own impl FromText.impl Zeitpunkt { rule zone -> Zone = ... }.grammar, and the impl points at its entry.match T::kind. It embeds the type the same way:
T: FromText => "\"" v:T "\"" -> { return v }
T: FromNumber => v:T -> { return v }
When a type implements several traits, the order of the format’s arms decides which one is used. No supertraits are needed (those stay undecided, ADR-341 §6).
rule from_text -> T = s:text -> { return schnell_lesen(s) }. The compiler needs nothing special for this.json::Value as a field and @json::Rest (ADR-344 D4) were not discussed. A first reading: json::Value could implement a trait of the json package, which would make the exact-type arm of option B unnecessary. That is not decided yet.
rule items in traits and impls, the shorthand, rules named by type, the derived function, and bound-guarded arms. ADR-grammar-2 D12 and §6 point to it.rule item in a trait (signature only).rule item in a trait impl or an inherent impl, lowered to a one-entry grammar.v:T under a bound T: FromText resolves the same way.T: Trait => in match T::kind. Arm order is tried top to bottom.T::from_text(s). This includes the call on a type parameter, which is refused today: fn read[T: FromText](s: String) -> T throws { return T::from_text(s) } gives NK1245.std: the FromText and FromNumber traits. The json package gets the bound-guarded arms. time::Instant gets impl FromText.json::RawText) and their undecoded input..nika programs in tests/compiler/ for each step.Generated by Claude Code
Owner, 2026-11-03: #1020 falls under the specification-first exception (#1214). Where the code it changes is Rust (the parser’s grammar block, the checker), it is built in Rust. Recorded in CLAUDE.md.
Generated by Claude Code
Owner, 2026-11-03: json::Value as a field is not a special case. It is an enum whose variants are recognised by the shape of the value: a string can only be Text, an object only Object, and so on. It needs no exact-type arm and no trait of its own. The form of enums with payload variants is decided in #1219, and the json::Value field is built there.
@json::Rest remains outside this decision. It concerns how a struct treats members it does not know, not the text form of a type.
Generated by Claude Code