Avaje Jsonb

Discord Source API Docs Issues Releases
Discord Github Javadoc Github



This is a light (~200kb + generated code), fast, and reflection free Json binding library

Default Supported Types

Jsonb has built-in support for reading and writing Java’s core data types:

Quick Start

1. Add avaje-jsonb dependencies.

<dependency>
  <groupId>io.avaje</groupId>
  <artifactId>avaje-jsonb</artifactId>
  <version>${avaje.jsonb.version}</version>
</dependency>
<!-- if using spring web, add the below to use jsonb for http messaging -->
<dependency>
  <groupId>io.avaje</groupId>
  <artifactId>avaje-jsonb-spring-starter</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>
  <optional>true</optional>
</dependency>

2a. JDK 23+

In JDK 23+, annotation processors are disabled by default, so we need to add a compiler property to re-enable.

<properties>
  <maven.compiler.proc>full</maven.compiler.proc>
</properties>

3. Add @Json onto types we want to serialize.


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 {
  private String street;
  private String suburb;
  private String city;
  // object fields will automatically have adapters generated, no @Json required
  // (though you can add @Json anyway to modify the generated adapter how you wish)
  private OtherClass other;
  //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))");

// serialize to json the above specified properties only
String asJson = myView.toJson(customer);

Spring/Avaje Inject Integration

When used with Spring or Avaje Inject, a default Jsonb instance will be provided and used for serializing/deserializing Http messages. The following properties can be added to configure the default instance.

jsonb.deserialize.failOnUnknown (default false)
jsonb.serialize.mathTypesAsString (default false)
jsonb.serialize.empty (default true)
jsonb.serialize.nulls (default false)

Java Module Setup

If using java modules, in the module-info.java we need to:

  1. Add a requires clause for io.avaje.jsonb
  2. Add a provides clause for io.avaje.jsonb.Jsonb.GeneratedComponent
Example module-info
import io.avaje.jsonb.Jsonb.GeneratedComponent;

module org.example {

  requires io.avaje.jsonb;

  // you must define the fully qualified class name of the generated classes. if you use an import statement, compilation will fail
  provides GeneratedComponent with org.example.jsonb.GeneratedComponent;

  //if running using Jlink application images with avaje-inject add:
  //requires io.avaje.jsonb.plugin;
}

In the example above, org.example.jsonb.GeneratedComponent is generated code typically found in target/generated-sources/annotations.

@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 Code: (click to expand)
@Generated
public final class AddressJsonAdapter implements JsonAdapter<Address>, ViewBuilderAware {

  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", City.class));
    builder.add("suburb", suburbJsonAdapter, builder.method(Address.class, "getSuburb", Suburb.class));
    builder.endObject();
  }

  @Override
  public void toJson(JsonWriter writer, Address address) {
    writer.beginObject(names);
    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(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.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.Property

We can override the serialization/deserialization name of a field using @Json.Property.

@Json
public class Customer {

  @Json.Property("SomeOtherName")
  private String name;
  ...
}

Effectively, we have renamed this property and will not be able to deserialize this 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: (click to expand)

@Json.Property makes the following changes to the generated JsonAdapter:

@Generated
public final class CustomerJsonAdapter implements JsonAdapter<Customer>, 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(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.Alias

We can define a deserialization alias for a field using @Json.Alias. It is compatible with, and can work in tandem with @Property

@Json
public class Customer {

  @Alias({"SomeOtherName","SomeOtherName2"})
  private name
  ...
}
Generated Code: (click to expand)

@Json.Alias makes the following changes to the generated JsonAdapter:

@Generated
public final class CustomerJsonAdapter implements JsonAdapter<Customer>, ViewBuilderAware {

  ...

  @Override
  public Customer fromJson(JsonReader reader) {
    Customer _$customer = new Customer();

    // read json
    reader.beginObject(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.Creator

With @Json.Creator, we can override deserialization using a constructor or static factory method. Contructor/Method parameters can be annotated with @Alias to rename a deserialization field.

@Json
public record Kingfisher(@Json.Alias("species") String name, int fishCaught) {

  @Json.Creator
  public static Kingfisher construct(String name) {
    return new Kingfisher(name, 42);
  }
}
Generated Code: (click to expand)

@Json.Creator makes the following changes to the generated JsonAdapter:

@Generated
public final class KingfisherJsonAdapter implements JsonAdapter<Kingfisher>, ViewBuilderAware {

  ...

  @Override
  public Kingfisher fromJson(JsonReader reader) {
    // variables to read json values into, constructor params don't need _set$ flags
    String     _val$name = null;

    // read json
    reader.beginObject(names);
    while (reader.hasNextField()) {
      final String fieldName = reader.nextField();
      switch (fieldName) {
        case "name":
        case "species":
          _val$name = stringJsonAdapter.fromJson(reader);
          break;

        case "fishCaught":
          reader.skipValue(); //value ignored since not in factory method
          break;

        default:
          reader.unmappedField(fieldName);
          reader.skipValue();
      }
    }
    reader.endObject();

    // build and return Kingfisher
    Kingfisher _$kingfisher = Kingfisher.construct(_val$name);
    return _$kingfisher;
  }
}

@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: (click to expand)

@Json.Ignore makes the following changes to the generated JsonAdapter:

@Generated
public final class SecretsJsonAdapter implements JsonAdapter<Secrets>, ViewBuilderAware {

  ...

  @Override
  public void toJson(JsonWriter writer, Secrets secrets) {
    writer.beginObject(names);
    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(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: (click to expand)

@Json.Unmapped makes the following changes to the generated JsonAdapter:

@Generated
public final class UnmappedJsonJsonAdapter implements JsonAdapter<UnmappedJson>, ViewBuilderAware {

  ...

  @Override
  public void toJson(JsonWriter writer, UnmappedJson unmappedJson) {
    writer.beginObject(names);
    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(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 to specify a method that will provide the value used to serialize to/from json.

Inlining Classes

When using @Json.Value on a class method, a special adapter is generated that will use this value to (de)serialize.

In the example below, the class is serialized as it were a String object.

public class Inlined {
  private final String value;
  private final int otherValue; //notUsed

  public Inlined(String value) {
    this.value = value;
  }

  @Json.Value
  public String value() {
    return value;
  }
}
Generated Code: (click to expand)

The specialized adapter generated to (de)serialize the object:

@Generated
public final class InlinedJsonAdapter implements JsonAdapter<Inlined> {

  private final JsonAdapter<String> adapter;

  public InlinedJsonAdapter(Jsonb jsonb) {
    this.adapter = jsonb.adapter(String.class);
  }

  @Override
  public void toJson(JsonWriter writer, Inlined value) {
    adapter.toJson(writer, value.value());
  }

  @Override
  public Inlined fromJson(JsonReader reader) {
    return new Inlined(adapter.fromJson(reader));
  }
}

Enum Mapping with @Json.Value

When using @Json.Value with Enum methods, a specialized adapter using an EnumMap will be generated to cache the constant values for (de)serialization. This works for any method return type that can be compared with .equals()

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;
  }
}
Generated Code: (click to expand)
@Generated
public final class MyEnumJsonAdapter implements JsonAdapter<MyEnum> {

  private static final Map<MyEnum, String> toValue = new EnumMap<>(MyEnum.class);
  private static final Map<String, MyEnum> toEnum = new HashMap<>();
  private final JsonAdapter<String> adapter;

  public MyEnumJsonAdapter(Jsonb jsonb) {
    this.adapter = jsonb.adapter(String.class);
    if(!toValue.isEmpty()) return;
    for(final var enumConst : MyEnum.values()) {
      var val = enumConst.getCode();
      toValue.put(enumConst, val);
      if(toEnum.containsKey(val)) throw new IllegalArgumentException("Duplicate value "+ val + " from enum method value. @Json.Value methods must return unique values");
      toEnum.put(val, enumConst);
    }
  }

  @Override
  public void toJson(JsonWriter writer, MyEnum value) {
    adapter.toJson(writer, toValue.get(value));
  }

  @Override
  public MyEnum fromJson(JsonReader reader) {
    final var value = adapter.fromJson(reader);
    final var enumConstant = toEnum.get(value);
    if (enumConstant == null)
      throw new JsonDataException("Unable to determine MyEnum enum value for " + value);
    return enumConstant;
  }
}

@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: (click to expand)

@Json.Mixin makes the following changes to the generated MixinTargetJsonAdapter:

@Generated
public final class MixinTargetJsonAdapter implements JsonAdapter<MixinTarget>, 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(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(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.

By default the "type property" that specifies the type in json is "@type". Use @Json(typeProperty=...) to specify the name of the type property.

Note: There is a current limitation that polymorphic types do not yet support "Json Views".

@Json
@Json.SubType(type = Car.class, name = "CAR")
@Json.SubType(type = Truck.class, name = "TRUCK")
public abstract class Vehicle {
  private String engine;
  ...
}

If desired, you can define the type parameter yourself for your own use.

@Json(typeProperty="type")
@Json.SubType(type = Car.class, name = "CAR")
@Json.SubType(type = Truck.class, name = "TRUCK")
public abstract class Vehicle {
  //can use getters/setters on the typeProperty
  //the value will be for deserialization only
  private String type;
  ...
}
//For interfaces, the typeProperty needs to be defined in the subClasses

Additionally, you can deserialize the typeProperty to an enum.

enum TypeEnum {
  CAR,
  TRUCK;
}

@Json(typeProperty="type")
//the subType names must correspond to an enum constant
@Json.SubType(type = Car.class, name = "CAR")
@Json.SubType(type = Truck.class, name = "TRUCK")
public abstract class Vehicle {
  private TypeEnum type;
  ...
}
Generated Code: (click to expand)

Given this class:

@Json
@Json.SubType(type = Car.class)
@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;
  ...
}

The below adapter will be generated:

@Generated
public final class VehicleJsonAdapter implements 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(names);
    if (vehicle instanceof Car) {
      Car sub = (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 Truck) {
      Truck sub = (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(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)) {
      Car _$vehicle = new Car();
      if (_set$engine) _$vehicle.setEngine(_val$engine);
      if (_set$carField) _$vehicle.setCarField(_val$carField);
      return _$vehicle;
    }
    if ("TRUCK".equals(type)) {
      Truck _$vehicle = new 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);
  }
}

@CustomAdapter

With @CustomAdapter, you can define your own JsonAdapter for your more esoteric serialization needs. A custom adapter registered using this annotation must have a public constructor accepting a Jsonb instance (or a public static JsonAdapter.Factory FACTORY field for generic adapters), and must directly implement the JsonAdapter Interface.

Standard Adapters

@CustomAdapter
public class CustomClassJsonAdapter implements JsonAdapter<CustomClass> {

  private final JsonAdapter<String> stringJsonAdapter;
  private final PropertyNames names;

  //a public construtor accepting Jsonb is required
  public CustomClassJsonAdapter(Jsonb jsonb) {
    //type adapters can be fetched using adapter
    this.stringJsonAdapter = jsonb.adapter(String.class);
    //write the json field names
    this.names = jsonb.properties("body");
  }

 ... write your custom serialization logic
}

Generic Adapters

@CustomAdapter(isGeneric = true)
public class CustomGenericClassJsonAdapter<T> implements JsonAdapter<GenericClass<T>> {

  private final JsonAdapter<T> TAdapter;
  private final PropertyNames names;

  // Required
  public static final JsonAdapter.Factory FACTORY =
      (type, jsonb) -> {
        if (Types.isGenericTypeOf(type, GenericClass.class)) {

          return new CustomGenericClassJsonAdapter<>(jsonb, Types.typeArguments(type));
        }
        return null;
      };

  public CustomGenericClassJsonAdapter(Jsonb jsonb, Type[] types) {
    this.TAdapter = jsonb.adapter(types[0]);
    this.names = jsonb.properties("key", "val");
  }

  ... write your custom serialization logic
}