Home > database >  Jackson: deserialize missing field to Kotlin/Java List
Jackson: deserialize missing field to Kotlin/Java List

Time:08-14

I have the answer to my own question, so I post both the answer and solution, as explicitly encouraged by Jeff Atwood. My question was originally for Kotlin, but while trying to find a solution, I also tried Java, so I provide the question and solution in both Java and Kotlin.)

Question in Kotlin

Given this deserializable Product class:

data class Product(val name: String, val prices: List<Int>)

and this json string that lacks the prices field:

{"name": "Computer"}

how can I deserialize the json string to a Product object using Jackson?

What I have tried in Kotlin

I tried this:

data class Product(val name: String, val prices: List<Int>)

// Missing "prices" field
val json = """{"name": "Computer"}"""

// "prices" field included works fine
// val json = """{"name": "Computer", "prices": [1,2,3]}"""

val mapper = ObjectMapper().registerKotlinModule()
val product = mapper.readValue<Product>(json)
println(product)

but it results in this exception:

com.fasterxml.jackson.module.kotlin.MissingKotlinParameterException: Instantiation of 
[simple type, class MainKt$main$Product] value failed for JSON property prices due to 
missing (therefore NULL) value for creator parameter prices which is a non-nullable type
 at [Source: (String)"{"name": "Computer"}"; line: 1, column: 20] 
(through reference chain: MainKt$main$Product["prices"])

When using Java

For Java the Product class would be:

class Product {
    private String name;
    private List<Integer> prices;

    public Product(String name, List<Integer> prices) {
        this.name = name;
        this.prices = prices;
    }

    @Override
    public String toString() {
        return "Product{name='"   name   "\", prices="   prices   '}';
    }
}

with this Jackson code:

String json = "{\"name\": \"Computer\"}";
// String json = "{\"name\": \"Computer\", \"prices\": [1,2,3]}";

ObjectMapper mapper = new ObjectMapper();
// ParameterNamesModule is needed for non-arg constructor when not using Jackson annotations
mapper.registerModule(new ParameterNamesModule());
Product product = mapper.readValue(json, Product.class);

// Shows "prices=null", while "prices=[]" is required
System.out.println(product);

But this sets prices to null instead of an empty list.

CodePudding user response:

Solution in Kotlin

This solution is for Jackson 2.11 and higher. It uses the jackson-module-kotlin Maven artifact.

val kotlinModule = KotlinModule.Builder()
    .configure(KotlinFeature.NullToEmptyCollection, true)
    .build()
val mapper = ObjectMapper().registerModule(kotlinModule)

val product = mapper.readValue(json, Product::class.java)
println(product)

So the solution uses KotlinFeature.NullToEmptyCollection, which has the following documentation:

Default: false. Whether to deserialize null values for collection properties as empty collections.

There is also a map version: KotlinFeature.NullToEmptyMap.

For version 2.9 and 2.10 you can use the nullToEmptyCollection default parameter of the KotlinModule constructor.

Solution in Java using annotations

Annotated Product class:

class Product {
    private String name;
    private List<Integer> prices;

    public Product(@JsonProperty("name") String name, 
                   @JsonProperty("prices") 
                   @JsonSetter(nulls = Nulls.AS_EMPTY) List<Integer> prices
    ) {
        this.name = name;
        this.prices = prices;
    }

    @Override
    public String toString() {
        return "Product{name='"   name   "\', prices="   prices   '}';
    }
}

Jackson code:

String json = "{\"name\": \"Computer\"}";
ObjectMapper mapper = new ObjectMapper();
Product product = mapper.readValue(json, Product.class);
System.out.println(product); // Product{name='Computer', prices=[]}

The key part in this solution is @JsonSetter(nulls = Nulls.AS_EMPTY), which sets the missing or null json field to an empty list in Java.

The number of verbose annotations, such as @JsonProperty("prices") can be reduced by using the jackson-module-parameter-names Maven artifact. Then only @JsonSetter(nulls = Nulls.AS_EMPTY) is needed.

Solution in Java without annotations

This solution requires the jackson-module-parameter-names Maven artifact. When using this module/artifact, don't forget to add the -parameters compiler argument.

Product class Jackson without annotations:

class Product {
    private String name;
    private List<Integer> prices;

    public Product(String name, List<Integer> prices) {
        this.name = name;
        this.prices = prices;
    }

    @Override
    public String toString() {
        return "Product{name='"   name   "\", prices="   prices   '}';
    }
}

Jackson code:

String json = "{\"name\": \"Computer\"}";

ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new ParameterNamesModule());
mapper.setDefaultSetterInfo(JsonSetter.Value.forValueNulls(Nulls.AS_EMPTY));

Product product = mapper.readValue(json, Product.class);
System.out.println(product);

The ParameterNamesModule model is required to allow Jackson to reflect the Product constructor parameters by name, so that @JsonProperty("prices") isn't required anymore. And JsonSetter.Value.forValueNulls(Nulls.AS_EMPTY) is used to convert missing or null json fields to a list.

CodePudding user response:

Your Product class need to implement Serializable class. It can make consistency data.

class Product implements Serializable {
    ..............
}
  • Related