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
- 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)
- Has no fallback to reflection - avaje is code generation or bust.
- Constructors and accessors/getters/setters of any style should all "just work" (record type, constructors, 'fluid setters' all just work)
- Jackson-like annotations
- Provide support for dynamic json views
- Supports importing and generating adapters for library types
- Supports Jackson-like mixins
- Supports generic types
Default Supported Types
Jsonb has built-in support for reading and writing Java’s core data types:
- Primitives (int, float, char...) and their boxed counterparts (Integer, Float, Character...).
- BigInteger and BigDecimal
- Date and java.time classes (Instant, LocalDate, LocalDateTime...)
- Arrays, Collections, Streams, Lists, Sets, and Maps
- Strings
- Enums
- java.net classes (URL, URI, InetAddress...)
- Other miscellaneous types (UUID...)
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:
- Add a requires clause for io.avaje.jsonb
- Add a provides clause for io.avaje.jsonb.spi.JsonbExtension
Example module-info
import io.avaje.jsonb.spi.JsonbExtension;
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 JsonbExtension 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
Say we want to override the field serialization behavior on a class we can't modify (For example, a class in an external
project/dependency). We can use @Mixin
to add Jsonb Annotations to the specified type.
public class Kingfisher {
private String name;
//getters/setters...
}
Given the above class, we can use the @Json.Mixin
annotation on an abstract class to effectively add @Json annotations and even change how the class is constructed
@MixIn(Kingfisher.class)
public abstract class KingfisherMixin {
@Json.Property("species")
String name;
//we can even change how the target is constructed with @Json.Creator
@Json.Creator
static Kingfisher construct(String name) {
return new Kingfisher(species);
}
}
Generated Code: (click to expand)
@Json.Mixin
makes the following changes to the generated MixinTargetJsonAdapter:
@Generated("io.avaje.jsonb.generator")
public final class KingfisherJsonAdapter implements JsonAdapter<Kingfisher>, ViewBuilderAware {
...
public KingfisherJsonAdapter(Jsonb jsonb) {
this.stringJsonAdapter = jsonb.adapter(String.class);
//name attribute replaced with species
this.names = jsonb.properties("species");
}
...
@Override
public void toJson(JsonWriter writer, Kingfisher kingfisher) {
writer.beginObject(names);
writer.name(0);
stringJsonAdapter.toJson(writer, kingfisher.getName());
writer.endObject();
}
@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 "species":
_val$name = stringJsonAdapter.fromJson(reader);
break;
default:
reader.unmappedField(fieldName);
reader.skipValue();
}
}
reader.endObject();
// build and return Kingfisher using the mixin creator
Kingfisher _$kingfisher = KingfisherMixin.construct(_val$name);
return _$kingfisher;
}
}
@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"
. To specify alternate values for the type property, use
@Json(typeProperty=...)
.
@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
}
@Serializer
A case may arise where you need custom (de)serialization for a certain field without affecting the serialization for a type globally. This is the use case for @Serializer
.
@Json
public class Example {
@Json.Serializer(MoneySerializer.class)
BigDecimal amountOwed;
...
}
A non-global custom adapter must be created and registered with @CustomAdapter(global = false)
. This signals that this adapter is exclusive for @Serializer
use.
@CustomAdapter(global = false)
public class MoneySerializer implements JsonAdapter<BigDecimal> {
@Override
public BigDecimal fromJson(JsonReader reader) {
return reader.readDecimal().setScale(2, RoundingMode.DOWN);
}
@Override
public void toJson(JsonWriter writer, BigDecimal value) {
writer.value(value.setScale(2, RoundingMode.DOWN));
}
}