Avaje Jsonb

License Source API Docs Issues Releases
Apache2 Github Javadoc Github



This is a light, fast, and reflection free Json binding library

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);
  }
}