I am using Immutables library (https://immutables.github.io).
My class looks as follows:
package com.abc.myservice.data.models;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import org.immutables.value.Value;
import java.util.Map;
import java.util.Optional;
#Value.Immutable
#JsonSerialize(as = ImmutableMyEntityModel.class)
#JsonDeserialize(as = ImmutableMyEntityModel.class)
public interface MyEntityModel {
String myEntityId();
String status();
Optional<Integer> count();
Optional<Integer> version();
Optional<Map<String, String>> attributes();
}
I build the immutable class object with:
ImmutableMyEntityModel.builder()
.myEntityId("some-id")
.status("some-status")
.count(Optional.of(10))
.build()
And my output is:
{
"MyEntityId": "some-id",
"status": "some-status",
"count": {
"present": true
},
"version": {
"present": false
},
"attributes": {
"present": false
}
}
Instead what I would like to see is:
{
"MyEntityId": "some-id",
"status": "some-status",
"count": 10
}
How can I make it work like that?
Use the jackson-datatype-jdk8 module so that Jackson properly understands the java.util.Optional type - a pretty good explanation is in this article.
Add jackson-datatype-jdk8 library to your project/classpath, which contains a Jackson module that allows Jackson to properly understand Optionals.
When creating an ObjectMapper, register the Jdk8Module:
ObjectMapper om = new ObjectMapper();
om.registerModule(new Jdk8Module());
Optionally, add #JsonIgnoreProperties(ignoreUnknown = true) to properties or the class itself to avoid serializing Optional.empty() to null values and instead ignore the property completely.
Full example:
public class JacksonOptionalTest
{
public static void main(String... args)
throws Exception
{
ObjectMapper om = new ObjectMapper();
om.registerModule(new Jdk8Module());
Thing thing = new Thing();
thing.name = "John Smith";
thing.count = Optional.of(12);
String s = om.writeValueAsString(thing);
System.out.println(s);
}
#JsonInclude(Include.NON_ABSENT)
#JsonIgnoreProperties(ignoreUnknown = true)
public static class Thing
{
public String name;
public Optional<Integer> count = Optional.empty();
public Optional<Integer> version = Optional.empty();
}
}
The output of this is {"name":"John Smith","count":12}.
Related
I'm using Jackson to parse JSON data on a Spring Boot application, but Jackson is unable to select properly the JSON fields to map to the POJO fields.
Details
Firstly, I'm reading data from a third party library, and their data model has a lot of redundancy (namely the several properties can represent the same information), and I can't ask them to fix it.
Their JSON is something like that:
{ "name" : "name", "full_name" : "name", "fullName" : "name"}
there are 3 properties in JSON containing the same information. But sometimes there would be only one of these properties which is non-null, like:
{ "name" : null, "full_name" : "", "fullName" : "name"}
And that happens to many other properties.
I've tried to use #JsonAlias to extract the required (non-null) data from the incoming JSON, but it doesn't resolve the issue.
#JsonProperty("name")
#JsonAlias({"full_name","fullName"})
private String name;
First, that #JsonAlias is taking precedence from the #JsonProperty value. Second, it's not ignoring null values.
How can I make Jackson ignore null values in a situation like described above?
Multiple Setters
One of the possible options is to define a bunch of additional setters for each duplicated property, annotated with #JsonSetter to map each setter to a corresponding property flavor.
To avoid logic duplication, every additional setter should delegate to a regular setter, which should determine if the existing value needs to be updated (if it's empty, null, etc.).
public class MyPojo {
public static final Predicate<String> NULL_OR_EMPTY =
s -> s == null || s.isEmpty(); // predicate can be reused to check multiple properties
private String name;
#JsonSetter("name")
public void setName(String name) {
if (NULL_OR_EMPTY.test(this.name)) this.name = name;
}
#JsonSetter("full_name")
public void setName1(String name) {
setName(name);
}
#JsonSetter("fullName")
public void setName2(String name) {
setName(name);
}
}
Usage example:
String json = "{ "name" : null, "full_name" : "", "fullName" : "name"}";
ObjectMapper mapper = new ObjectMapper();
MyPojo myPojo = mapper.readValue(json, MyPojo.class);
System.out.println(myPojo);
Output:
MyPojo{name='name'}
The drawbacks of this approach are the usage of "smart-setters", which might not considered to be clean (because it violates the Single responsibility principle, setters aren't meant to perform validation) and domain class gets polluted with additional setter methods. Both issues can be solved by externalizing this functionality.
Custom Converter
Another possible solution is to customize deserialization by defining a Converter for the target type.
Note: don't confuse Converter and Deserializer. Deserializer is meant to provide logic how to construct a POJO based on the information contained in the JsonParser, whilst Converter is used to transform one POJO (which is easier to deserialize) into another POJO.
So we need to create two classes: Converter and auxiliary type reflecting the data model of the incoming JSON.
Consider the following auxiliary POJO:
public record AuxiliaryPojo(
#JsonProperty("name") String name,
#JsonProperty("full_name") String name1,
#JsonProperty("fullName") String name2
) {}
And that's the Converter extending StdConverter that bridges AuxiliaryPojo and MyPojo:
public class AuxiliaryPojoToMyPojo extends StdConverter<AuxiliaryPojo, MyPojo> {
public static final Predicate<String> NOT_NULL_OR_EMPTY =
s -> s != null && !s.isEmpty();
#Override
public MyPojo convert(AuxiliaryPojo v) {
return MyPojo.builder()
.name(findMatching(v.name(), v.name1(), v.name2()))
.build();
}
private String findMatching(String... args) {
return Arrays.stream(args)
.filter(NOT_NULL_OR_EMPTY)
.findFirst().orElse(null);
}
}
And here's the domain class (free from any redundant code). Note that Converter has been specified via converter property of the #JsonDeserialize annotation.
#Getter
#Builder
#JsonDeserialize(converter = AuxiliaryPojoToMyPojo.class)
public static class MyPojo {
private String name;
}
That would be enough to parse the sample JSON into an instance of MyPojo:
String json = "{ "name" : null, "full_name" : "", "fullName" : "name"}";
ObjectMapper mapper = new ObjectMapper();
MyPojo myPojo = mapper.readValue(json,bMyPojo.class);
System.out.println(myPojo);
Output:
MyPojo{name='name'}
An other solution is to ignore additionnal fields 'fullName' and 'full_name' using annotation #JsonIgnoreProperties and a custom deserializer that throws an exception when name property is null (or equal to string "null") so that you can catch that exception in order not to create a person when name is null.
See code below:
Class Person
import java.util.ArrayList;
import java.util.List;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
#JsonIgnoreProperties({ "fullName", "full_name" })
public class Person {
#JsonProperty("name")
#JsonDeserialize(using = CustomNameDeserializer.class)
private String name;
public Person() {
super();
}
public String toString() {
return "name: " + name;
}
public static void main(String[] args) {
String[] personArrayJson = new String[3];
personArrayJson[0]="{ \"name\" : \"nameNotNull1\", \"full_name\" : \"name\", \"fullName\" : \"name\"}";
personArrayJson[1]="{ \"name\" : \"null\", \"full_name\" : \"\", \"fullName\" : \"name\"}";
personArrayJson[2]="{ \"name\" : \"nameNotNull2\", \"full_name\" : \"name\", \"fullName\" : \"name\"}";
List<Person> persons = new ArrayList<Person>();
ObjectMapper mapper = new ObjectMapper();
Person p;
for (String personJson : personArrayJson) {
try {
p = mapper.readValue(personJson, Person.class);
persons.add(p);
} catch (JsonProcessingException e) {
//e.printStackTrace();
System.out.println("NAme is null: not deserialized");
}
}
System.out.println("Persons list contains "+persons.size()+" persons => "+persons);
}
}
Class CustomNameDeserializer
import java.io.IOException;
import com.fasterxml.jackson.core.JacksonException;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
public class CustomNameDeserializer extends StdDeserializer<String> {
public CustomNameDeserializer(Class<String> s) {
super(s);
}
public CustomNameDeserializer() {
this(null);
}
#Override
public String deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JacksonException, NullPointerException{
String n = p.getValueAsString();
if(n==null || n.equals("null") || n.trim().equals(""))
throw new NullPointerException("Name is null");
return n;
}
}
Hope it helps
I have a Java record with one field only:
public record AggregateId(UUID id) {}
And a class with the AggregateId field (other fields removed for readability)
public class Aggregate {
public final AggregateId aggregateId;
#JsonCreator
public Aggregate(
#JsonProperty("aggregateId") AggregateId aggregateId
) {
this.aggregateId = aggregateId;
}
}
The implementation above serialize and deserialize JSON with given example:
ObjectMapper objectMapper = new ObjectMapper();
String content = """
{
"aggregateId": {
"id": "3f61aede-83dd-4049-a6ff-337887b6b807"
}
}
""";
Aggregate aggregate = objectMapper.readValue(content, Aggregate.class);
System.out.println(objectMapper.writeValueAsString(aggregate));
How could I change Jackson config to replace JSON by that:
{
"aggregateId": "3f61aede-83dd-4049-a6ff-337887b6b807"
}
without giving up a separate class for AggregateId and access through fields, without getters?
I tried #JsonUnwrapper annotation, but this caused throws
Exception in thread "X" com.fasterxml.jackson.databind.exc.InvalidDefinitionException:
Invalid type definition for type `X`:
Cannot define Creator parameter as `#JsonUnwrapped`: combination not yet supported at [Source: (String)"{
"aggregateId": "3f61aede-83dd-4049-a6ff-337887b6b807"
}"
or
Exception in thread "X" com.fasterxml.jackson.databind.exc.InvalidDefinitionException:
Cannot define Creator property "aggregateId" as `#JsonUnwrapped`:
combination not yet supported at [Source: (String)"{
"aggregateId": "3f61aede-83dd-4049-a6ff-337887b6b807"
}"
Jackson version: 2.13.1
dependencies {
compile "com.fasterxml.jackson.core:jackson-annotations:2.13.1"
compile "com.fasterxml.jackson.core:jackson-databind:2.13.1"
}
Of course, it's possible with a custom serializer/deserializer, but I'm looking for an easier solution because I have many different classes with a similar issue.
The combination of #JsonUnwrapped and #JsonCreator is not supported yet, so we can generate a solution like this:
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonUnwrapped;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import java.util.UUID;
public class AggregateTest {
static record AggregateId(#JsonProperty("aggregateId") UUID id) {}
static class Aggregate {
#JsonUnwrapped
#JsonProperty(access = JsonProperty.Access.READ_ONLY)
public final AggregateId _aggregateId;
public final String otherField;
#JsonCreator
public Aggregate(#JsonProperty("aggregateId") UUID aggregateId,
#JsonProperty("otherField") String otherField) {
this._aggregateId = new AggregateId(aggregateId);
this.otherField = otherField;
}
}
public static void main(String[] args) throws JsonProcessingException {
String rawJson =
"{\"aggregateId\": \"1f61aede-83dd-4049-a6ff-337887b6b807\"," +
"\"otherField\": \"İsmail Y.\"}";
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
Aggregate aggregate = objectMapper
.readValue(rawJson, Aggregate.class);
System.out.println(objectMapper
.writeValueAsString(aggregate));
}
}
Here we briefly get rid of the #JsonUnwrapped field.
We get the UUID with the name aggregateId and create an AggregateId record.
Detailed explanations about it:
https://github.com/FasterXML/jackson-databind/issues/1467
https://github.com/FasterXML/jackson-databind/issues/1497
How do I create the following json using java classes and lombok's builder?
I used some json to pojo tool and created 2 classes: Entry.java and Testplan.java, added a method to convert String to json and managed to get a json object: {"suite_id":99,"name":"Some random name"}
I don't understand how to create one that would look like this:
{
"name": "System test",
"entries": [
{
"suite_id": 1,
"name": "Custom run name"
},
{
"suite_id": 1,
"include_all": false,
"case_ids": [
1,
2,
3,
5
]
}
]
}
Testplan.java
#Data
#Builder
public class Testplan {
#JsonProperty("name")
public String name;
#JsonProperty("entries")
public List<Entry> entries = null;
}
Entry.java
#Data
#Builder
#NoArgsConstructor
#AllArgsConstructor
#JsonInclude(JsonInclude.Include.NON_NULL)
public class Entry {
#JsonProperty("suite_id")
public Integer suiteId;
#JsonProperty("name")
public String name;
#JsonProperty("include_all")
public Boolean includeAll;
#JsonProperty("case_ids")
public List<Integer> caseIds = null;
}
I convert String to json using this:
public <U> String toJson(U request) throws JsonProcessingException {
ObjectMapper mapper = new ObjectMapper()
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
return mapper.writeValueAsString(request);
}
Here's how I started creating the object and got stuck:
public static Entry getRequestTemplate() {
Entry entry = new Entry();
entry.setName("Here's some name");
entry.setSuiteId(16);
return entry;
}
To see what's happening I added this:
#Test
public void showJson() throws JsonProcessingException {
String json = toJson(getRequestTemplate());
System.out.println(json);
}
I expect to have have to combine these two classes and create a list of case_ids but can't wrap my head around it.
This worked:
Created a new method of Testplan:
public Testplan kek2() {
Testplan testplan = Testplan.builder()
.name("System test")
.entries(Lists.newArrayList(Entry.builder()
.name("Custom run name")
.suiteId(1)
.includeAll(false)
.caseIds(new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5)))
.build()))
.build();
System.out.println(testplan);
return testplan;
}
and then used this method to convert pojo to json:
protected <U> String toJson(U request) throws JsonProcessingException {
ObjectMapper mapper = new ObjectMapper();
return mapper.writeValueAsString(request);
}
I am trying to get the correct JSON for
public class MyTestResponse {
#XmlElementWrapper(name = "data")
#XmlElement(name = "values")
public List<String> test = Arrays.asList("Sidney");
}
I now get
"data": [
"Sidney"
],
instead of
"data":{
"values": [
"Sidney"
]
},
I am using org.codehaus.jackson stack (1.9.0) inside ServiceMix 7 M3.
My JSON provider extends org.codehaus.jackson.jaxrs.JacksonJaxbJsonProvider:
import org.codehaus.jackson.jaxrs.JacksonJaxbJsonProvider;
import org.codehaus.jackson.map.AnnotationIntrospector;
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.map.SerializationConfig;
import org.codehaus.jackson.map.introspect.JacksonAnnotationIntrospector;
import org.codehaus.jackson.xc.JaxbAnnotationIntrospector;
public class MyJsonProvider extends JacksonJaxbJsonProvider {
public JsonProvider() {
super();
ObjectMapper mapper = new ObjectMapper();
AnnotationIntrospector primary = new JaxbAnnotationIntrospector();
AnnotationIntrospector secondary = new JacksonAnnotationIntrospector();
AnnotationIntrospector pair = new AnnotationIntrospector.Pair(secondary, primary);
mapper.getDeserializationConfig().setAnnotationIntrospector(pair);
mapper.getSerializationConfig().setAnnotationIntrospector(pair);
this.setMapper(mapper);
}
}
How can I tell the JacksonJaxbJsonProvider not to replace the XmlElement name but tow wrap it?
As you have discovered JSON marshalling doesn’t honor JAXB annotations. Enable the following MapperFeature on your jackson ObjectMapper (your instance named mapper)
mapper.enable(MapperFeature.USE_WRAPPER_NAME_AS_PROPERTY_NAME);
Is there a way to have the configuration of SerializationFeature.WRAP_ROOT_VALUE as an annotation on the root element instead using ObjectMapper?
For example I have:
#JsonRootName(value = "user")
public class UserWithRoot {
public int id;
public String name;
}
Using ObjectMapper:
#Test
public void whenSerializingUsingJsonRootName_thenCorrect()
throws JsonProcessingException {
UserWithRoot user = new User(1, "John");
ObjectMapper mapper = new ObjectMapper();
mapper.enable(SerializationFeature.WRAP_ROOT_VALUE);
String result = mapper.writeValueAsString(user);
assertThat(result, containsString("John"));
assertThat(result, containsString("user"));
}
Result:
{
"user":{
"id":1,
"name":"John"
}
}
Is there a way to have this SerializationFeature as an annotation and not as an configuration on the objectMapper?
Using dependency:
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.7.2</version>
</dependency>
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.JsonTypeName;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
public class Test2 {
public static void main(String[] args) throws JsonProcessingException {
UserWithRoot user = new UserWithRoot(1, "John");
ObjectMapper objectMapper = new ObjectMapper();
String userJson = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(user);
System.out.println(userJson);
}
#JsonTypeName(value = "user")
#JsonTypeInfo(include = JsonTypeInfo.As.WRAPPER_OBJECT, use = JsonTypeInfo.Id.NAME)
private static class UserWithRoot {
public int id;
public String name;
}
}
#JsonTypeName and #JsonTypeInfo together make it possible.
Result:
{
"user" : {
"id" : 1,
"name" : "John"
}
}
I think this has been requested as:
https://github.com/FasterXML/jackson-databind/issues/1022
so if anyone wants a challenge & a chance to make many users happy (it is something that'd be nice to have for sure), it's up for grabs :)
Aside from that one small thing worth noting is that you can use ObjectWriter to enable/disable SerializationFeatures.
String json = objectMapper.writer()
.with(SerializationFeature.WRAP_ROOT_VALUE)
.writeValueAsString(value);
in case you need to sometimes use this, other times not (ObjectMapper settings should not be changed after initial construction and configuration).
You can use the it in the below way, so this property will be applied to throughout the objectMapper usage.
static {
objectMapper.configure(SerializationFeature.WRAP_ROOT_VALUE, true);
}