Add typed canonical JSON encoding

This commit is contained in:
sergeych committed 2026-04-25 00:36:18 +03:00
1 parent 2bedaa0969
commit 2abe7e2f96
4 files changed
+660 -57

No files matched your search

+58 -2
View File
@@ -8,15 +8,22 @@ Lyng now has two distinct JSON-facing layers:
- canonical JSON round-trip format:
- `Json.encode(value)`
- `Json.decode(text)`
- typed canonical JSON round-trip format:
- `Json.encodeAs(Type, value)`
- `Json.decodeAs(Type, text)`
Use the first when you need ordinary JSON for interop.
Use the second when you need Lyng value round-trip semantics through JSON text.
Use the second when you need Lyng value round-trip semantics through JSON text with no schema.
Use the third when both sides already know the Lyng type and you want the same round-trip semantics with fewer type
tags in the JSON.
This distinction is intentional:
- plain JSON projection is optimized for compatibility with ordinary JSON tooling
- canonical `Json.encode()` is optimized for semantic fidelity to Lyng and Lynon
- canonical `Json.encode()` is optimized for semantic fidelity to Lyng and Lynon and stays self-describing
- typed canonical `Json.encodeAs()` is optimized for the same fidelity when the schema is provided externally
- these goals conflict for values such as sets, exceptions, singleton objects, buffers, and maps with non-string keys
## Plain JSON projection in Lyng
@@ -93,6 +100,9 @@ Please note that `toJsonString` should be used to get serialized string represen
They still use JSON text, but they add Lyng-specific type tags where plain JSON would otherwise lose information.
When a map already fits ordinary JSON object rules, canonical JSON keeps that traditional object shape. In particular,
maps with string keys are still serialized as JSON objects, not as tagged entry lists.
Example:
```lyng
@@ -124,6 +134,41 @@ The plain `toJson()` projection is intended for ordinary JSON interop.
Canonical `Json.encode()` should be read as the JSON analogue of `Lynon.encode()`: when Lynon already preserves a
Lyng distinction, canonical JSON tries to preserve it too, using tags only where ordinary JSON is insufficient.
## Typed canonical Json round-trip format
`Json.encodeAs(Type, value)` and `Json.decodeAs(Type, text)` use the same canonical rules, but with a declared target
type available during the whole traversal.
This changes one thing only: type tags may be omitted when the declared type is already exact enough to restore the
value unambiguously.
The same map rule still applies here: `Map<String, T>` stays a normal JSON object, while non-string-key maps fall back
to canonical entry encoding.
Example:
```lyng
import lyng.serialization
closed class Point(x: Int, y: Int)
closed class Segment(a: Point, b: Point)
val value = Segment(Point(0, 1), Point(2, 3))
val encoded = Json.encodeAs(Segment, value)
assertEquals("{\"a\":{\"x\":0,\"y\":1},\"b\":{\"x\":2,\"y\":3}}", encoded)
assertEquals(value, Json.decodeAs(Segment, encoded))
```
Subtype information is still preserved when the declared type is wider than the runtime one. For example, if a field is
declared as `Base` but contains `Derived`, canonical subtype tags remain in that field.
This is why the APIs are split:
- `toJson()` stays plain and interop-friendly
- `Json.encode()` stays fully self-describing and safe to decode without a schema
- `Json.encodeAs()` uses the supplied schema to reduce noise, but only where that schema is sufficient
## Kotlin side interfaces
The "Batteries included" principle is also applied to serialization.
@@ -241,6 +286,17 @@ This format can also round-trip:
- non-finite reals
- `void`
### Typed canonical `Json.encodeAs`
This format round-trips the same value space as canonical `Json.encode`, but it can emit simpler JSON for:
- closed classes and other exactly-known class fields
- enums when the enum type is known
- typed collections whose element types are known
- nested object graphs where declared field types are precise
It still falls back to canonical tagged encoding when exact runtime type information would otherwise be lost.
It does so by adding Lyng-specific type tags only when necessary.
## Kotlin-side extension point for more formats