I am trying to make jackson deserialize missing properties that represent collections into empty collections instead of NULL. I've tried a few different things and this is the latest that isn't working.
If there is a way to make any potential solution globally configured for all POJO deserialization that would be great.
Given the following json, which is missing the property 'assets':
{
"name":"my-layer",
"code": "ly1",
"types":["type1", "type2"],
"private": false
}
Given the following POJO:
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonSetter;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Builder.Default;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
#Data
#Builder
#AllArgsConstructor
#NoArgsConstructor
#EqualsAndHashCode
public class CreateLayerRequest {
#NotBlank
#JsonProperty(required = true)
private String name;
#NotBlank
#JsonProperty(required = true)
private String code;
#Default
#NotNull
#JsonProperty(value = "private", required = true)
private Boolean privateLayer = Boolean.FALSE;
#Default
#JsonSetter(nulls = Nulls.AS_EMPTY)
#JsonProperty("types")
private Set<UUID> types = new HashSet<>();
#Default
#JsonSetter(nulls = Nulls.AS_EMPTY)
#JsonProperty("assets")
private Set<UUID> assets = new HashSet<>();
}
configured my fix to be global
#Configuration
public class MyAppConfiguration implements WebMvcConfigurer {
/**
* allows POJOs using json filtering to serialize all properties when not being filtered
*/
#Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
for (HttpMessageConverter<?> converter : converters) {
if (converter instanceof MappingJackson2HttpMessageConverter) {
((MappingJackson2HttpMessageConverter) converter)
.getObjectMapper()
// not related to this thread, this is for property filtering when serializing POJOs
.setFilterProvider(new SimpleFilterProvider().setFailOnUnknownId(false))
// these modules are related to to this thread
.registerModule(
new SimpleModule().addDeserializer(Set.class, new CollectionDeserializer())
)
.registerModule(
new SimpleModule().addDeserializer(List.class, new CollectionDeserializer())
)
.registerModule(
new SimpleModule().addDeserializer(Map.class, new CustomMapDeserializer())
);
}
}
}
#Slf4j
#NoArgsConstructor
#AllArgsConstructor
private static class CollectionDeserializer<T>
extends JsonDeserializer<Collection<T>>
implements ContextualDeserializer {
private JavaType type;
private final ObjectMapper mapper = new ObjectMapper();
#Override
public JsonDeserializer<?> createContextual(
DeserializationContext deserializationContext,
BeanProperty beanProperty) {
final JavaType javaType = deserializationContext.getContextualType() != null
? deserializationContext.getContextualType()
: beanProperty.getMember().getType();
return new CollectionDeserializer<>(javaType);
}
#Override
public Collection<T> deserialize(
JsonParser jsonParser,
DeserializationContext deserializationContext) throws IOException {
try {
if (Set.class.isAssignableFrom(type.getRawClass())) {
return deserializeJsonToCollection(jsonParser, new HashSet<>());
}
if (List.class.isAssignableFrom(type.getRawClass())) {
return deserializeJsonToCollection(jsonParser, new ArrayList<>());
}
} catch (IllegalArgumentException e) {
log.warn("unable to deserialize array property in request");
throw e;
}
final String message = "unable to deserialize collection";
log.warn(message);
throw new CollectionDeserializationException(message);
}
private Collection<T> deserializeJsonToCollection(
JsonParser jsonParser,
Collection<T> collection) throws IOException {
((ArrayNode) jsonParser
.getCodec()
.readTree(jsonParser))
.forEach(item -> {
try {
final T value = mapper.readValue(
String.format("\"%s\"", item.textValue()),
type.getContentType()
);
collection.add(value);
} catch (IOException e) {
final String message = String.format(
"unable to deserialize value [%s] to type [%]",
item.textValue(),
type.getContentType()
);
log.warn(message);
throw new CollectionDeserializationException(message);
}
});
return collection;
}
#Override
public Collection<T> getNullValue(DeserializationContext ctxt) {
if (Set.class.isAssignableFrom(type.getRawClass())) {
return new HashSet<>();
}
if (List.class.isAssignableFrom(type.getRawClass())) {
return new ArrayList<>();
}
return null;
}
}
#Slf4j
private static class CustomMapDeserializer<T, K> extends JsonDeserializer<Map<T, K>> {
private final ObjectMapper mapper = new ObjectMapper();
private final TypeReference<Map<T, K>> mapTypeReference = new TypeReference<>() {
};
#Override
public Map<T, K> deserialize(
JsonParser jsonParser,
DeserializationContext deserializationContext) {
try {
return mapper.readValue(jsonParser, mapTypeReference);
} catch (Exception e) {
final String message = "unable to deserialize map";
log.warn(message);
throw new MapDeserializationException(message, e);
}
}
#Override
public Map<T, K> getNullValue(DeserializationContext ctxt) {
return new HashMap<>();
}
}
Related
Edit:
The problem could be isolated and is described here Jackson: Deserialize to a Map<String, Object> with correct type for each value
First off, there is a work around using enableDefaultTyping(ObjectMapper.DefaultTyping.OBJECT_AND_NON_CONCRETE, JsonTypeInfo.As.PROPERTY);. But that is not really applicable because the method is deprecated for obvious reasons and it also adds unnecessary type information.
Edit:
I stripped down the code to a bare minimum and I also found the problem. The container has Object as values which Jackson seems to treat as simple Object during serialization. That might be the reason why the annotations at class level are not being considered.
#JsonProperty("simulation_parameters")
private final Map<Parameter<?>, Object> simulation_parameters;
The resulting JSON looks like this after serialization:
{
"simulation_parameters" : {
"INPUT_HOUSEHOLDS" : "input_hh",
"INPUT_PERSONS" : "input_p"
}
}
Changing the container to:
#JsonProperty("simulation_parameters")
private final Map<Parameter<?>, DataResource> simulation_parameters;
Results in:
{
"simulation_parameters" : {
"INPUT_HOUSEHOLDS" : {
"#type" : "database",
"resource_name" : "public.households"
},
"INPUT_PERSONS" : {
"#type" : "file",
"filename" : "d:/persons.csv"
}
}
}
Since I have type information in my map keys, is there a way to serialize an entire entry instead of serializing key and value seperately using the #JsonSerialize(keyUsing:..., contentUsing:...)?
import com.fasterxml.jackson.annotation.*;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.KeyDeserializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import java.io.IOException;
import java.util.*;
public class JacksonTest {
public static void main(String[] args) throws IOException {
ParameterContainer parameters = new ParameterContainer();
DataResource input_hh = new DatabaseResource("public.households");
parameters.addParameter(Parameters.INPUT_HOUSEHOLDS, input_hh);
DataResource input_p = new FileResource("d:/persons.csv");
parameters.addParameter(Parameters.INPUT_PERSONS, input_p);
ObjectMapper om = new ObjectMapper();
String json_missing_type = om.writerWithDefaultPrettyPrinter().writeValueAsString(parameters);
System.out.println(json_missing_type);
}
#JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.PROPERTY, //apparently this is redundant
property = "#type")
#JsonSubTypes({
#JsonSubTypes.Type(value = DatabaseResource.class, name = "database"),
#JsonSubTypes.Type(value = FileResource.class, name = "file")
})
public interface DataResource {
#JsonIgnore
String getResourceName();
}
//data resource as file
#JsonTypeName("file")
public static class FileResource implements DataResource {
#JsonProperty("filename")
private final String filename;
#JsonCreator
public FileResource( #JsonProperty("filename") String filename) {
this.filename = filename;
}
#JsonIgnore
#Override
public String getResourceName() {
return this.filename;
}
}
//data resource as database
#JsonTypeName("database")
public static class DatabaseResource implements DataResource {
#JsonProperty("resource_name")
private final String resource_name;
#JsonCreator
public DatabaseResource(#JsonProperty("resource_name") String resource_name) {
this.resource_name = resource_name;
}
#JsonIgnore
#Override
public String getResourceName() {
return resource_name;
}
}
public static class ParameterDeserializer extends KeyDeserializer {
#Override
public Object deserializeKey(String key, DeserializationContext ctxt) {
Optional<Parameter<?>> defined_parameter = Parameters.values.stream().filter(param -> param.getName().equals(key)).findAny();
if (defined_parameter.isPresent())
return defined_parameter.get();
else
throw new IllegalArgumentException("Parameter: " + key + " is not defined");
}
}
public static class ParameterContainer {
#JsonProperty("simulation_parameters")
#JsonSerialize(typing = JsonSerialize.Typing.DEFAULT_TYPING)
private final Map<Parameter<?>, DataResource> simulation_parameters;
public ParameterContainer() {
this.simulation_parameters = new HashMap<>();
}
#JsonCreator
public ParameterContainer(#JsonProperty("simulation_parameters") Map<Parameter<?>,DataResource> simulation_parameters) {
this.simulation_parameters = simulation_parameters;
}
#JsonIgnore
public void addParameter(Parameter<?> parameter,DataResource value) {
simulation_parameters.put(parameter, value);
}
}
public static class Parameter<T> {
#JsonValue
private final String name;
private final Class<T> object_type;
protected Parameter(String name, Class<T> object_type) {
this.name = name;
this.object_type = object_type;
}
public String getName() {
return name;
}
#JsonIgnore
public Class<T> getObjectType() {
return object_type;
}
}
public static final class Parameters {
public static final Parameter<DataResource> INPUT_HOUSEHOLDS = new Parameter<>("INPUT_HOUSEHOLDS", DataResource.class);
public static final Parameter<DataResource> INPUT_PERSONS = new Parameter<>("INPUT_PERSONS", DataResource.class);
public static final Set<Parameter<?>> values = Set.of(INPUT_HOUSEHOLDS, INPUT_PERSONS);
}
}
I am trying to unmarshal a json response which comes from a server. But I would like to know which is the best way, approach to use when the json response changes.
For example, if I have a json response like this:
{
"name": "John",
"last_name": "John Last name",
"date_of_birth": "01.01.1990"
}
With jackson, I could deserialize the json object into a Person.class like this:
#NoARgsConstructor
#Setter
public class Person(){
private String name;
private String lastName;
private String dateOfBirth;
}
But what if the json struct changes, and now the attributes of the person comes inside a person object.
{
"person": {
"name": "John",
"last_name": "John Last name",
"date_of_birth": "01.01.1990"
}
}
What would be the best way to avoid this things or to avoid this problems? Is there any possible solution or approach to implement in java spring?
How about searching in the JSON yourself?
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
public class Foo {
public static void main(String args[]) throws Exception {
String jsonString = "{\"foo\":\"bar\"}";
ObjectMapper mapper = new ObjectMapper();
ObjectNode node = mapper.readValue(jsonString, ObjectNode.class);
if(node.has("foo")) {
System.out.println("foo: " + node.get("foo"));
}
}
}
To make it completely dynamic, I have used reflection api and json-simple-1.1 jar and jackson
import com.fasterxml.jackson.annotation.JsonProperty;
public class Person {
#JsonProperty("name")
private String name;
#JsonProperty("last_name")
private String lastName;
#JsonProperty("date_of_birth")
private String dateOfBirth;
}
import java.beans.IntrospectionException;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Map;
import org.json.simple.JSONObject;
import org.json.simple.parser.JSONParser;
import org.json.simple.parser.ParseException;
import com.fasterxml.jackson.annotation.JsonProperty;
public class Demo {
public static void main(String a[]) {
try {
String json1 = "{\r\n" + " \"person\": {\r\n" + " \"name\": \"John\",\r\n"
+ " \"last_name\": \"John Last name\",\r\n" + " \"date_of_birth\": \"01.01.1990\"\r\n"
+ " } \r\n" + " }";
String json2 = "{\r\n" + " \"name\": \"John\",\r\n" + " \"last_name\": \"John Last name\",\r\n"
+ " \"date_of_birth\": \"01.01.1990\"\r\n" + "} ";
extractedPersonObject(json1);
System.out.println("*****************************");
extractedPersonObject(json2);
} catch (ParseException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
private static void extractedPersonObject(String json2) throws ParseException {
Person person = new Person();
Object obj = new JSONParser().parse(json2);
JSONObject jo = (JSONObject) obj;
Map finalMap;
Field[] fields = Person.class.getDeclaredFields();
if (jo.get(fields[0].getName()) == null) {
finalMap = ((Map) jo.get("person"));
} else
finalMap = (Map) jo;
for (Field field : fields) {
JsonProperty jsonProperty = field.getDeclaredAnnotation(JsonProperty.class);
invokeSetter(person, field.getName(), finalMap.get(jsonProperty.value()));
}
System.out.println(person.getDateOfBirth());
System.out.println(person.getLastName());
System.out.println(person.getName());
}
public static void invokeSetter(Object obj, String propertyName, Object variableValue) {
PropertyDescriptor pd;
try {
pd = new PropertyDescriptor(propertyName, obj.getClass());
Method setter = pd.getWriteMethod();
try {
setter.invoke(obj, variableValue);
} catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
e.printStackTrace();
}
} catch (IntrospectionException e) {
e.printStackTrace();
}
}
}
A generic way to deserialize such wrapped is to write a deserializer of your own, like:
#SuppressWarnings("serial")
public class UnwrappingDeserializer<T> extends StdDeserializer<T> {
#Setter
private String fieldName;
private ObjectMapper innerMapper = new ObjectMapper();
public UnwrappingDeserializer(Class<T> vc) {
super(vc);
fieldName = handledType().getSimpleName().toLowerCase();
}
#SuppressWarnings("unchecked")
#Override
public T deserialize(JsonParser p, DeserializationContext ctxt)
throws IOException, JsonProcessingException {
JsonNode rootT = p.readValueAsTree();
// Check if there is a node with specified field name.
// There is also a setter for it if the field name is not
// directly resolvable
JsonNode valueT = rootT.get(fieldName);
if (valueT == null) {
// If no such node it is the root tha has the value
valueT = rootT;
}
return innerMapper.convertValue(valueT, (Class<T>)handledType());
}
}
Assuming you have Person as:
#Getter #Setter
// Below is because of your JSON key format
#JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class)
public class Person {
private String name;
private String lastName;
private String dateOfBirth;
}
you can just add the deserializer to your ObjectMapper like:
ObjectMapper om = new ObjectMapper();
SimpleModule sm = new SimpleModule();
sm.addDeserializer(Person.class, new UnwrappingDeserializer<Person>(Person.class));
om.registerModule(sm);
We're using Jackson JSON mapper in our code to de-serialize some configuration objects. We'd like for Jackson to fail on de-serialization when specific fields are missing or empty
The only feature in Jackson to support this behavior is for primitives :
final DeserializationConfig.Feature failOnPremitives = DeserializationConfig.Feature.FAIL_ON_NULL_FOR_PRIMITIVES;
The thing is the fields in question are mainly strings
Any help is highly appreciated
There is an option called: FAIL_ON_NULL_FOR_CREATOR_PARAMETERS.
So I assume it would be accessible at: DeserializationConfig.Feature.FAIL_ON_NULL_FOR_CREATOR_PARAMETERS;
Or in yml:
jackson:
serialization:
indent-output: false
deserialization:
fail-on-unknown-properties: true
fail-on-missing-creator-properties: true
fail-on-null-creator-properties: true
This works on all types, strings, ints, doubles etc..
Have you considered Bean Validation?
While Jackson is focused in JSON parsing, Bean Validation is all about declaring and performing validation on your beans.
You could use #NotNull or #NotBlank from Hibernate Validator, the Bean Validation reference implementation.
Alternatively you could use JSON Schema.
Object specific custom Deserializer would have to be created.
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.ObjectCodec;
import com.fasterxml.jackson.core.Version;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
import com.fasterxml.jackson.databind.module.SimpleModule;
import java.io.IOException;
class JacksonDeserializerTest {
public static void main(String[] args) throws Exception {
ObjectMapper mapper = new ObjectMapper();
SimpleModule module = new SimpleModule("CustomPersonDeserializer", new Version(1, 0, 0, null, null, null));
module.addDeserializer(Person.class, new CustomPersonDeserializer());
mapper.registerModule(module);
String jsonString = "{ \"id\": 1, \"name\": \"User 1 \"}";
Person user = mapper.readValue(jsonString, Person.class);
System.out.println("User: " + user.toString());
jsonString = "{ \"id\": 1}";
user = mapper.readValue(jsonString, Person.class);
}
static class CustomPersonDeserializer extends StdDeserializer<Person> {
private static final long serialVersionUID = -4100181951833318756L;
public CustomPersonDeserializer() {
this(null);
}
public CustomPersonDeserializer(Class<?> vc) {
super(vc);
}
#Override
public Person deserialize(JsonParser parser, DeserializationContext deserializer) throws IOException, JsonProcessingException {
Person person = new Person();
ObjectCodec codec = parser.getCodec();
JsonNode node = codec.readTree(parser);
JsonNode idNode = node.get("id");
int id = idNode.asInt();
person.setId(id);
JsonNode nameNode = node.get("name");
if(nameNode == null){
throw new IOException("name must be provided");
}
String name = nameNode.asText();
if (name.trim().length() < 1){
throw new IOException("name can not be empty");
}
person.setName(name);
return person;
}
}
static class Person {
private int id;
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
#Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", id=" + id +
'}';
}
}
}
Alternate way:
This works on the objects as well
ObjectMapper mapper= new ObjectMapper();
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
I have the following Address and AddressList classes
public class Address {
private String street;
private String city;
private String state;
// ...
}
public class AddressList {
private List<Address> addresses;
// ...
}
and then a Person class
public class Person {
private String name;
#JsonDeserialize(contentUsing = ListDeserializer.class)
private Map<String, AddressList> addresses;
// ..
}
Then I have the Yaml file like this
---
name: 'abc'
addresses:
offices:
- street: 123 main st
city: san francisco
state: ca
- street: 234 post st
city: san francisco
state: ca
My List deserializer class is as follows:
public class ListDeserializer extends JsonDeserializer<AddressList> {
#Override
public AddressList deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JsonProcessingException {
return jsonParser.readValueAs(new TypeReference<Map<String, List<Address>>>() {
});
}
}
my parsing code is as follows:
ObjectMapper objectMapper = new ObjectMapper(new YAMLFactory());
return objectMapper.readValue(inputYamlFile, Person.class);
when I read the list of addresses: it prints as
{offices=null}
Can anyone help with resolving this?
If it was serialized as a list you have to deserialize it the same way. But you then have the list and and can iterate it yourself.
I found a way to resolve this issue using deserializing addresses map using Converter.
The modifications I made are
public class Person {
private String name;
#JsonDeserialize(converter = AddressListConverter.class)
private Map<String, AddressList> addresses;
// ..
}
I, then, wrote a new converter class for the AddressList.
public class AddressListConverter implements Converter<Map<String, List<LinkedHashMap>>, Map<String, AddressList>> {
#Override
public Map<String, AddressList> convert(Map<String, List<LinkedHashMap>> stringListMap) {
Map<String, AddressList> addressListMap = new HashMap<>();
ObjectMapper mapper = new ObjectMapper();
for (Map.Entry<String, List<LinkedHashMap>> entry : stringListMap.entrySet()) {
AddressList addressList = new AddressList();
for(LinkedHashMap map: entry.getValue()) {
Address address = mapper.convert(map, Address.class);
addressList.getAddresses().add(address);
}
addressListMap.put(entry.getKey(), addressList);
}
return addressListMap;
}
#Override
public JavaType getInputType(TypeFactory typeFactory) {
return typeFactory.constructMapType(Map.class, String.class, List.class);
}
#Override
public JavaType getOutputType(TypeFactory typeFactory) {
return typeFactory.constructMapType(Map.class, String.class, AddressList.class);
}
}
This should do the trick
I have json that contains multiple rows.
{
"rows": [{
"aId": 408,
"aFrstNm": "TIM",
},
{
"aId": 410,
"aFrstNm": "BOB",
},
{
"aId": 409,
"aFrstNm": "HENRY",
}]
}
and POJOs
public class User extends Employee {
#JsonProperty("tableName")
private Double aID;
#JsonProperty("aFrstNm")
private String aFrstNm;
getters and setters ommitted
}
public class Employee {
}
public class Employees {
private Collection<? extends Employee> rows;
getters and setters ommitted
}
What I am trying to do is to pass the User class to a custom deserializer to be able return employees as a collection of Users that extend Employee:
SimpleModule simpleModule = new SimpleModule("MyModule")
.addDeserializer(Employees.class, new JsonDeserializer<Employees>() {
#Override
public Employees deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
ObjectMapper mapper = (ObjectMapper) p.getCodec();
final Class<?> clazz = (Class<?>) ctxt.getAttribute("mappingClass");
final JsonNode jsonNode = (JsonNode) mapper.readTree(p).get("rows");
Employees employees = new Employees();
Collection<Product> rows = new ArrayList();
if (jsonNode.isArray()) for (final JsonNode objNode : jsonNode) {
boolean isValueNode = objNode.isValueNode();
String json = mapper.writeValueAsString(objNode);
Product product = (Product) new ObjectMapper().readValue(json, clazz);
rows.add(product);
System.out.println(jsonNode);
}
employees.setRows(rows);
return employees;
}
});
mapper.registerModule(simpleModule);
the variable clazz (final Class clazz) attribute should contain the class a parameter. I so far have been unable to find a mechanism that allows this. DeserializationContext seems to be promising but I have not been able to make this work. Any suggestions on using DeserializationContext or some other way to pass parameters?
Tnx.