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), and containers: the
List/Set/Map/Deque interfaces and their implementations, and arrays, with the object
elements inside them bound as well (the element type is written into the field descriptor by
the compiler and Field.getElementType() reads it; reflection itself still has no generic
types). Like Gson, char is written as a single-character string and read back from a string
too.
GsonBuilder's knobs (serializeNulls, setPrettyPrinting, setLenient, the five
setFieldNamingPolicy policies, excludeFieldsWithModifiers,
excludeFieldsWithoutExposeAnnotation, setVersion), the filtering and renaming of
@Expose/@Since/@Until/@SerializedName, the JsonSerializer/JsonDeserializer
adapters (registerTypeAdapter/registerTypeHierarchyAdapter/@JsonAdapter) and the
streaming JsonReader/JsonWriter are all there (t166_gson_builder,
t167_gson_annotations, t168_gson_adapters; containers are t157_json_collections and
t165_json_container_elements).
What is still not supported: element types are recorded one level deep (the inner
element of a List<List<Person>> is read from the document's shape, and an object or array
inside it is refused by name; the same goes for an element declared Object), an array whose
component is ? super or an unbounded type variable has no class to build an array from (the
member is refused by name), @SerializedName's alternate (several names are written as
repeated annotations: the first is the one written out, the rest are also accepted on
input), and @JsonAdapter names the adapter class as a string.
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.