Teyru

JSON and the Gson Compatibility Layer

The Gson-shaped tree API, plus object binding generated by the compiler.

Teyru's JSON comes in two layers: lib/10_json.teyru is Gson's tree API (pure Teyru, no compiler cooperation needed), and lib/19_gson.teyru adds the compiler's object binding.

Tree API (lib/10)

JsonElement, JsonObject, JsonArray, JsonPrimitive, JsonNull, JsonParser.parseString, Gson, GsonBuilder, JsonSyntaxException, with behavior aligned to Gson 2.10, including several details that are easy to get wrong:

  • JsonNull extends JsonElement rather than JsonPrimitive.
  • Parsed numbers keep the original literal, so 1e5 prints back as 1e5 and does not become 100000.0.
  • Escaping follows Gson's substitution table: " and \ are escaped, the five short forms \b \t \n \f \r, everything else below 0x20 uses \u00xx (lowercase hex), / is not escaped, and non-ASCII is emitted as UTF-8 rather than \u escapes, with the sole exceptions of U+2028/U+2029; <, >, &, =, ' are escaped only in the htmlSafe writing (the default new Gson()), while JsonElement.toString() does not escape them.
  • Empty objects and empty arrays stay on one line when pretty printed ({}, []).

Differences from Gson (deliberate):

  • JsonParser.parseString is strict (RFC 8259). The {a:1}, 'single quotes', 01, +1, .5, 1., NaN, [1,] that Gson's lenient mode accepts are all rejected and throw JsonSyntaxException; an empty document is rejected too, and trailing extra data is always rejected.
  • entrySet() returns List<JsonMember> and keySet() returns List<String> (Gson gives a Set); JsonMember's getKey/getValue correspond to Map.Entry.
  • Numbers only go up to long; there is no getAsBigDecimal/getAsBigInteger.
  • Numeric accessors throw IllegalArgumentException rather than NumberFormatException (the former is the latter's superclass, so code that catches the superclass is unaffected; a catch that names NumberFormatException will not catch them).

Object binding (lib/19 plus the compiler)

Gson uses reflection to bind objects to JSON. Here the compiler does it instead:

import teyru.*

class Address {
  String city
  String zip
}

class Person {
  String name
  int age
  @SerializedName("home_address") Address address
}

Gson gson = new Gson()
Person p = gson.fromJson(json, Person.class)   // no cast needed
String out = gson.toJson(p)

When the compiler sees fromJson(s, Person.class), it generates __teyruJsonRead(JsonElement) and __teyruJsonWrite() on Person, and rewrites the call into them. Therefore:

  • No cast needed: Gson's <T> T fromJson(String, Class<T>) cannot be expressed in Teyru (Class is not generic), but after the rewrite the return type is the target class itself.
  • A field type with no mapping is a compile error (TY-TYP-0110; prelude classes such as List/Map are TY-TYP-0108), rather than an IllegalArgumentException thrown at runtime from deep inside a reflection adapter.
  • Binding is generated per call site: if it is not used, it is not generated, and the field list is the one at generation time (Lombok-generated members included).

Supported field types: String, boolean/byte/short/char/int/long/float/ double and their boxed classes, enum (written as the constant name, read back by comparing names, the same as Gson; when the name does not match Gson leaves null, while this throws JsonParseException), other bindable classes (recursive), Object (preserves the original tree: a JsonObject that was read in is written back as-is). Like Gson, char is written as a single-character string and read back from a string too. @SerializedName can rename it. Not supported: List/Map/array fields, @Expose/@Since/@Until/@JsonAdapter (declared but not implemented, so using them has no effect).

Runtime fallback table

For a reference whose static type is Object (gson.toJson(someObject)), the compiler cannot see through, so this JsonBinding table is used: every class that has had a binding generated registers a reader/writer in its own static initializer, and lookup uses the object's own class — which is exactly the answer Gson's reflection gives.

Object can hold any class in the program, so as soon as such a call site appears, the compiler generates a binding for every class declared in the program (except interfaces, abstract classes, and classes that have no no-arg constructor and cannot be read from JSON; those that cannot be generated are not registered, and a runtime query for them reports that there is no binding). A program without this call site still generates bindings only for the classes actually used.

  • tests/programs/t93_json.teyru — tree API: escaping, numbers, pretty print, round-trip.
  • tests/programs/t101_gson.teyru — object binding: nested classes, @SerializedName, missing fields, runtime fallback table.
  • tests/programs/t140_json_binding_edges.teyru — edges: char round-trip, the tree inside an Object field, classes passed only through Object, enum and unknown constant names.

On this page