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:
JsonNullextendsJsonElementrather thanJsonPrimitive.- Parsed numbers keep the original literal, so
1e5prints back as1e5and does not become100000.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\uescapes, with the sole exceptions of U+2028/U+2029;<,>,&,=,'are escaped only in the htmlSafe writing (the defaultnew Gson()), whileJsonElement.toString()does not escape them. - Empty objects and empty arrays stay on one line when pretty printed (
{},[]).
Differences from Gson (deliberate):
JsonParser.parseStringis strict (RFC 8259). The{a:1},'single quotes',01,+1,.5,1.,NaN,[1,]that Gson's lenient mode accepts are all rejected and throwJsonSyntaxException; an empty document is rejected too, and trailing extra data is always rejected.entrySet()returnsList<JsonMember>andkeySet()returnsList<String>(Gson gives aSet);JsonMember'sgetKey/getValuecorrespond toMap.Entry.- Numbers only go up to
long; there is nogetAsBigDecimal/getAsBigInteger. - Numeric accessors throw
IllegalArgumentExceptionrather thanNumberFormatException(the former is the latter's superclass, so code that catches the superclass is unaffected; acatchthat namesNumberFormatExceptionwill 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 (Classis 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 asList/MapareTY-TYP-0108), rather than anIllegalArgumentExceptionthrown 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.
Related tests
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:charround-trip, the tree inside anObjectfield, classes passed only throughObject, enum and unknown constant names.
Lombok compatibility layer
The compiler's built-in Lombok compatibility layer: annotations are expanded into ordinary Teyru members during semantic analysis, with no annotation processor required.
Framework: a Spring-shaped container and web layer
Dependency injection and request binding are both done at compile time, instead of scanning with reflection at startup the way Spring does.