Avaje Jsonb
License | Source | API Docs | Issues | Releases |
---|---|---|---|---|
Apache2 | Github | Javadoc | Github |
This is a light, fast, and reflection free Json binding library
- Use Java annotation processing to generate java source for adapting JSON to/from java objects
- No need to manually register generated adapters. (Uses service loading to auto-register)
- Constructors and accessors/getters/setters of any style should all "just work" (record type, constructors, 'fluid setters' all just work)
- Provide support for dynamic json views
- Supports importing types and mixins
Quick Start
1. Add avaje.jsonb dependencies.
<dependency>
<groupId>io.avaje</groupId>
<artifactId>avaje.jsonb</artifactId>
<version>${avaje.jsonb.version}</version>
</dependency>
2. Add the annotation processor to your pom.
<!-- Annotation processors -->
<dependency>
<groupId>io.avaje</groupId>
<artifactId>avaje.jsonb-generator</artifactId>
<version>${avaje.jsonb.version}</version>
<scope>provided</scope>
</dependency>
Note that if there are other annotation processors and they are specified via maven-compiler-plugin annotationProcessorPaths then we add avaje-jsonb-generator there instead.
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths> <!-- All annotation processors specified here -->
<path>
<groupId>io.avaje</groupId>
<artifactId>avaje.jsonb-generator</artifactId>
<version>${avaje.jsonb.version}</version>
</path>
<path>
... other annotation processor ...
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
3. Add @Json
onto types we want to serialise.
The avaje-jsonb-generator annotation processor will generate a JsonAdapter as java source code for each type annotated with @Json
.
These will be automatically registered with Jsonb using a service loader mechanism.
For types we can not annotate with @Json
we can instead use @Json.Import
.
@Json
public class Address {
String street;
String suburb;
String city;
//getters/setters ommited for brevity
}
Also works with records:
@Json
public record Address(String street, String suburb, String city){}
4. Serialize/Deserialize your JSON/POJO
// build using defaults
Jsonb jsonb = Jsonb.builder().build();
JsonType<Customer> customerType = jsonb.type(Customer.class);
// If the type is generic we can specify
// JsonType<Customer<T1,T2,...>> customerType = jsonb.type(Types.newParameterizedType(Customer.class, T1.class,T2.class, ...);
Customer customer = ...;
// serialize to json
String asJson = customerType.toJson(customer);
// deserialize from json
Customer customer = customerType.fromJson(asJson);
5. JsonViews
This library supports dynamic json views which allow us to specify which specific properties to include when serialising to json.
Jsonb jsonb = Jsonb.builder().build();
JsonType<Customer> customerType = jsonb.type(Customer.class);
// only including the id and name
JsonView<Customer> idAndNameView = customerType.view("(id, name)");
String asJson = idAndNameView.toJson(customer);
JsonView<Customer> myView =
customerType.view("(id, name, billingAddress(*), contacts(lastName, email))");
// serialise to json the above specified properties only
String asJson = myView.toJson(customer);
@Json
Types with @Json
are picked up by avaje-jsonb-generator at compile time and a JsonAdapter is generated as java source code typically in target/generated-sources/annotations.
Constructors
The types can be a record/class and have constructors. When types do not have a default constructor (e.g. record types) then the generated code will use the constructor. Fields in the constructor do not need or use setter methods.
//Example record - all fields set via constructor
@Json
public record Address(String street, String suburb, String city) { }
All the fields of record types are set via constructor - no setters here.
When a class has a constructor like the City example below, then fields in the constructor do not need or use a setter method. We only need a setter method for fields that are not in the constructor.
@Json
public class City {
UUID id;
String name;
String zone;
public City(UUID id, String name) {
this.id = id;
this.name = name;
}
public setZone(String zone) {
this.zone = zone;
}
// plus getters ...
}
In the example above the id and name fields are set via constructor and only zone is set via setter method.
Setter methods
Fields that are not set via the constructor need to have a setter methods. There are 4 styles of setter methods that avaje-jsonb-generator will find.
// traditional setter
public void setSuburb(String suburb) {
this.suburb = suburb;
}
// accessor style setter
public void suburb(String suburb) {
this.suburb = suburb;
}
// fluid traditional setter
public Address setSuburb(String suburb) {
this.suburb = suburb;
return this;
}
// fluid accessor style setter
public Address suburb(String suburb) {
this.suburb = suburb;
return this;
}
Naming Convention
We can specify a naming convention via the naming attribute of @Json
.
This naming convention translates field names to json property names.
The result of changing the naming convention can be seen in the generated JsonAdapter code.
@Json(naming = LowerHyphen)
public class Customer {
...
}
//The Naming options are below with the default of Match.
enum Naming {
Match,
LowerHyphen,
LowerUnderscore,
LowerSpace,
UpperCamel,
UpperHyphen,
UpperUnderscore,
UpperSpace
}
Generated JsonAdapter
Given the class:
@Json
public class Address {
private String street;
private City city;
private Suburb suburb;
//getters/setters ommited for brevity
}
The following JsonAdapter is generated:
@Generated
public final class AddressJsonAdapter extends JsonAdapter<Address> implements ViewBuilderAware {
// naming convention Match
// street [java.lang.String] name:street setter:setStreet
// city [com.jojo.javalin.api.models.City] name:city setter:setCity
// suburb [com.jojo.javalin.api.models.Suburb] name:suburb setter:setSuburb
private final JsonAdapter<String> stringJsonAdapter;
private final JsonAdapter<City> cityJsonAdapter;
private final JsonAdapter<Suburb> suburbJsonAdapter;
private final PropertyNames names;
public AddressJsonAdapter(Jsonb jsonb) {
this.stringJsonAdapter = jsonb.adapter(String.class);
this.cityJsonAdapter = jsonb.adapter(City.class);
this.suburbJsonAdapter = jsonb.adapter(Suburb.class);
this.names = jsonb.properties("street", "city", "suburb");
}
@Override
public boolean isViewBuilderAware() {
return true;
}
@Override
public ViewBuilderAware viewBuild() {
return this;
}
@Override
public void build(ViewBuilder builder, String name, MethodHandle handle) {
builder.beginObject(name, handle);
builder.add("street", stringJsonAdapter, builder.method(Address.class, "getStreet", java.lang.String.class));
builder.add("city", cityJsonAdapter, builder.method(Address.class, "getCity", com.jojo.javalin.api.models.City.class));
builder.add("suburb", suburbJsonAdapter, builder.method(Address.class, "getSuburb", com.jojo.javalin.api.models.Suburb.class));
builder.endObject();
}
@Override
public void toJson(JsonWriter writer, Address address) {
writer.beginObject();
writer.names(names);
writer.name(0);
stringJsonAdapter.toJson(writer, address.getStreet());
writer.name(1);
cityJsonAdapter.toJson(writer, address.getCity());
writer.name(2);
suburbJsonAdapter.toJson(writer, address.getSuburb());
writer.endObject();
}
@Override
public Address fromJson(JsonReader reader) {
Address _$address = new Address();
// read json
reader.beginObject();
reader.names(names);
while (reader.hasNextField()) {
final String fieldName = reader.nextField();
switch (fieldName) {
case "street": {
_$address.setStreet(stringJsonAdapter.fromJson(reader)); break;
}
case "city": {
_$address.setCity(cityJsonAdapter.fromJson(reader)); break;
}
case "suburb": {
_$address.setSuburb(suburbJsonAdapter.fromJson(reader)); break;
}
default: {
reader.unmappedField(fieldName);
reader.skipValue();
}
}
}
reader.endObject();
return _$address;
}
}
@Json.Import
When we are unable to or do not wish to put @Json
on the types we can use @Json.Import
.
We can put @Json.Import
on a package or type and specify the types to generate a JsonAdapter for.
@Json.Import({Customer.class, Address.class, Order.class})
package org.example;
@Json.Property
We can override the serialization/deserialization name of a field using @Json.Property
.
@Json
public class Customer {
@Json.Property("SomeOtherName")
private name
...
}
Effectively, we have renamed this propert and will not be able to deserialize this property from a json of {"name":"Jolyne"}
.
It will now only deserialize for the new name. {"SomeOtherName":"Jolyne"}
.
If you wish to only specify an alias for the json property, use @Alias
.
Generated Code
@Json.Property
makes the following changes to the generated JsonAdapter:
@Generated
public final class CustomerJsonAdapter extends JsonAdapter<Customer> implements ViewBuilderAware {
...
public CustomerJsonAdapter(Jsonb jsonb) {
this.stringJsonAdapter = jsonb.adapter(String.class);
//observe how the property has been renamed from "name" to "SomeOtherName"
this.names = jsonb.properties("SomeOtherName");
}
...
@Override
public Customer fromJson(JsonReader reader) {
Customer _$customer = new Customer();
// read json
reader.beginObject();
reader.names(names);
while (reader.hasNextField()) {
final String fieldName = reader.nextField();
switch (fieldName) {
//observe how the property has been renamed from "name" to "SomeOtherName"
case "SomeOtherName": {
_$customer.setName(stringJsonAdapter.fromJson(reader)); break;
}
default: {
reader.unmappedField(fieldName);
reader.skipValue();
}
}
}
return _$customer;
}
}
@Json.JsonAlias
We can define a deserialization alias for a field using @Json.JsonAlias
. It is compatible with, and can work in tandem with @Property
@Json
public class Customer {
@JsonAlias({"SomeOtherName","SomeOtherName2"})
private name
...
}
Generated Code
@Json.JsonAlias
makes the following changes to the generated JsonAdapter:
@Generated
public final class CustomerJsonAdapter extends JsonAdapter<Customer> implements ViewBuilderAware {
...
@Override
public Customer fromJson(JsonReader reader) {
Customer _$customer = new Customer();
// read json
reader.beginObject();
reader.names(names);
while (reader.hasNextField()) {
final String fieldName = reader.nextField();
switch (fieldName) {
//observe how the alias property names have been added to the switch
case "SomeOtherName":
case "SomeOtherName2":
case "name": { {
_$customer.setName(stringJsonAdapter.fromJson(reader)); break;
}
default: {
reader.unmappedField(fieldName);
reader.skipValue();
}
}
}
return _$customer;
}
}
@Json.Ignore
We can exclude a field from json serialisation using @Json.Ignore
@Json
public class Secrets {
@Json.Ignore private String mySecret;
// Exclude from de-serialization only
@Json.Ignore(serialize = true)
private String mySecret2;
// Exclude from serialization only
@Json.Ignore(deserialize = true)
private String mySecret3;
//ommited getters/setters
}
Generated Code
@Json.Ignore
makes the following changes to the generated JsonAdapter:
@Generated
public final class SecretsJsonAdapter extends JsonAdapter<Secrets> implements ViewBuilderAware {
...
@Override
public void toJson(JsonWriter writer, Secrets secrets) {
writer.beginObject();
writer.names(names);
writer.name(1);
//only mySecret2 is serialized
stringJsonAdapter.toJson(writer, secrets.getMySecret2());
writer.endObject();
}
@Override
public Secrets fromJson(JsonReader reader) {
Secrets _$secrets = new Secrets();
// read json
reader.beginObject();
reader.names(names);
while (reader.hasNextField()) {
final String fieldName = reader.nextField();
switch (fieldName) {
case "mySecret": {
//skips deserializing ignored fields
reader.skipValue(); break;
}
case "mySecret2": {
reader.skipValue(); break;
}
case "mySecret3": {
//mySecret3 is the only field to get deserialized
_$secrets.setMySecret3(stringJsonAdapter.fromJson(reader)); break;
}
default: {
reader.unmappedField(fieldName);
reader.skipValue();
}
}
}
reader.endObject();
return _$secrets;
}
}
@Json.Unmapped
We can use @Json.Unmapped
to collect unmapped json during de-serialization and include it in serialization.
The @Json.Unmapped annotation must be on a field of type Map
@Json
public class UnmappedJson {
private String mapped;
@Json.Unmapped private Map<String, Object> unmapped;
}
Generated Code
@Json.Unmapped
makes the following changes to the generated JsonAdapter:
@Generated
public final class UnmappedJsonJsonAdapter extends JsonAdapter<UnmappedJson> implements ViewBuilderAware {
...
@Override
public void toJson(JsonWriter writer, UnmappedJson unmappedJson) {
writer.beginObject();
writer.names(names);
writer.name(0);
stringJsonAdapter.toJson(writer, unmappedJson.getMapped());
Map<String, Object> unmapped = unmappedJson.getUnmapped();
if (unmapped != null) {
for (Map.Entry<String, Object> entry : unmapped.entrySet()) {
writer.name(entry.getKey());
objectJsonAdapter.toJson(writer, entry.getValue());
}
}
writer.endObject();
}
@Override
public UnmappedJson fromJson(JsonReader reader) {
UnmappedJson _$unmappedJson = new UnmappedJson();
Map<String, Object> unmapped = new LinkedHashMap<>();
// read json
reader.beginObject();
reader.names(names);
while (reader.hasNextField()) {
final String fieldName = reader.nextField();
switch (fieldName) {
case "mapped": {
_$unmappedJson.setMapped(stringJsonAdapter.fromJson(reader)); break;
}
default: {
Object value = objectJsonAdapter.fromJson(reader);
unmapped.put(fieldName, value);
}
}
}
reader.endObject();
// unmappedField...
_$unmappedJson.setUnmapped(unmapped);
return _$unmappedJson;
}
}
@Json.Value
We can use @Json.Value
on Enum types to specify a method that provides the values that will be used to serialise to/from json. If the method returns a int type then it will be treated as json int and otherwise be treated as json string values.
In the example below the values used in the json content is "one value" and "two value" rather than the usual "ONE" and "TWO".
public enum MyEnum {
ONE("one value"),
TWO("two value");
final String val;
MyEnum(String val) {
this.val = val;
}
@Json.Value
public String value() {
return val;
}
}
@Json.Raw
We can use @Json.Raw
to mark a String field as containing raw JSON content. This is then read and written (as a string containing raw json).
@Json.Raw
String rawJson
@Json.Mixin
Mark this Class as a MixIn Type that can add Jsonb Annotations on the specified type.
Say we want to override the field serialization behavior on a class we can't modify.(Typically in an external project/dependency or otherwise)
public class MixinTarget {
private String name;
private String stand;
private String bandReference;
//getters/setters...
}
We can use the @Json.Mixin
annotation on an abstract class to effectively add @Json Annotations
@Json.MixIn(MixinTarget.class)
public abstract class MixinClass {
@Json.Property("part")
private String name;
@Json.Ignore
private String stand;
}
Generated Code
@Json.Mixin
makes the following changes to the generated MixinTargetJsonAdapter:
@Generated
public final class MixinTargetJsonAdapter extends JsonAdapter<MixinTarget> implements ViewBuilderAware {
...
public MixinTargetJsonAdapter(Jsonb jsonb) {
this.stringJsonAdapter = jsonb.adapter(String.class);
//the mixin class renamed "name" property to "part"
this.names = jsonb.properties("part", "stand", "bandReference");
}
...
@Override
public void toJson(JsonWriter writer, MixinTarget mixinTarget) {
writer.beginObject();
writer.names(names);
writer.name(0);
stringJsonAdapter.toJson(writer, mixinTarget.getName());
writer.name(2);
// stand property is absent
stringJsonAdapter.toJson(writer, mixinTarget.getBandReference());
writer.endObject();
}
@Override
public MixinTarget fromJson(JsonReader reader) {
MixinTarget _$mixinTarget = new MixinTarget();
// read json
reader.beginObject();
reader.names(names);
while (reader.hasNextField()) {
final String fieldName = reader.nextField();
switch (fieldName) {
case "part": {
_$mixinTarget.setName(stringJsonAdapter.fromJson(reader)); break;
}
case "stand": {
//now ignored
reader.skipValue(); break;
}
case "bandReference": {
_$mixinTarget.setBandReference(stringJsonAdapter.fromJson(reader)); break;
}
default: {
reader.unmappedField(fieldName);
reader.skipValue();
}
}
}
reader.endObject();
return _$mixinTarget;
}
}
@Json.Subtype
For mapping polymorphic types we specify on the parent type a @Json.Subtype
for each concrete sub-type that can represent that type.
Note: There is a current limitation that polymorphic types do not yet support "Json Views".
// By default the "type property" that specifies the type in json is @type. Use @Json(typeProperty=...) to specify the name of the type property.
@Json
@Json.SubType(type = Car.class, name = "CAR")
@Json.SubType(type = Truck.class, name = "TRUCK")
public abstract class Vehicle {
private String engine;
...
}
public class Car extends Vehicle {
private String carField;
...
}
public class Truck extends Vehicle {
private String truckfield;
...
}
Generated Code
@Json.Subtype
makes generates a JsonAdapter for the abstract class/interface Vehicle
:
@Generated
public final class VehicleJsonAdapter extends JsonAdapter<Vehicle> {
private final JsonAdapter<String> stringJsonAdapter;
private final PropertyNames names;
public VehicleJsonAdapter(Jsonb jsonb) {
this.stringJsonAdapter = jsonb.adapter(String.class);
//all the propertynames of the subclasses are included
this.names = jsonb.properties("@type", "engine", "carField", "truckfield");
}
@Override
public void toJson(JsonWriter writer, Vehicle vehicle) {
writer.beginObject();
writer.names(names);
if (vehicle instanceof com.jojo.javalin.api.models.Car) {
com.jojo.javalin.api.models.Car sub = (com.jojo.javalin.api.models.Car)vehicle;
writer.name(0);
stringJsonAdapter.toJson(writer, "CAR");
writer.name(1);
stringJsonAdapter.toJson(writer, sub.getEngine());
writer.name(2);
stringJsonAdapter.toJson(writer, sub.getCarField());
}
else if (vehicle instanceof com.jojo.javalin.api.models.Truck) {
com.jojo.javalin.api.models.Truck sub = (com.jojo.javalin.api.models.Truck)vehicle;
writer.name(0);
stringJsonAdapter.toJson(writer, "TRUCK");
writer.name(1);
stringJsonAdapter.toJson(writer, sub.getEngine());
writer.name(3);
stringJsonAdapter.toJson(writer, sub.getTruckfield());
}
writer.endObject();
}
@Override
public Vehicle fromJson(JsonReader reader) {
// variables to read json values into, constructor params don't need _set$ flags
String _val$engine = null; boolean _set$engine = false;
String _val$carField = null; boolean _set$carField = false;
String _val$truckfield = null; boolean _set$truckfield = false;
String type = null;
// read json
reader.beginObject();
reader.names(names);
while (reader.hasNextField()) {
final String fieldName = reader.nextField();
switch (fieldName) {
case "@type": {
type = stringJsonAdapter.fromJson(reader); break;
}
case "engine": {
_val$engine = stringJsonAdapter.fromJson(reader); _set$engine = true; break;
}
case "carField": {
_val$carField = stringJsonAdapter.fromJson(reader); _set$carField = true; break;
}
case "truckfield": {
_val$truckfield = stringJsonAdapter.fromJson(reader); _set$truckfield = true; break;
}
default: {
reader.unmappedField(fieldName);
reader.skipValue();
}
}
}
reader.endObject();
if (type == null) {
throw new IllegalStateException("Missing @type property which is required?");
}
if ("CAR".equals(type)) {
com.jojo.javalin.api.models.Car _$vehicle = new com.jojo.javalin.api.models.Car();
if (_set$engine) _$vehicle.setEngine(_val$engine);
if (_set$carField) _$vehicle.setCarField(_val$carField);
return _$vehicle;
}
if ("TRUCK".equals(type)) {
com.jojo.javalin.api.models.Truck _$vehicle = new com.jojo.javalin.api.models.Truck();
if (_set$engine) _$vehicle.setEngine(_val$engine);
if (_set$truckfield) _$vehicle.setTruckfield(_val$truckfield);
return _$vehicle;
}
throw new IllegalStateException("Unknown value for @type property " + type);
}
}