Jackson ignoring specific property but able to check was it availble - java

Is there a way to skip some properties on deserialization but at the same time knowing are they presented or not?
{
"id": 123,
"name": "My Name",
"picture": {
// a lot of properties that's not important for me
}
}
#JsonIgnoreProperties(ignoreUnknown=true)
#JsonInclude(JsonInclude.Include.NON_NULL)
public class User {
private int id;
}
So, I ignoreUnknown is what I want as a default behavior because I don't want name field and all other fields that can exist. The value of picture fields also is not important. I just want to know was picture property available or not. How I can do that?

You can add a boolean property and custom deserializer which just reads given value and returns true. Jackson invokes custom deserializer only if property exists in payload.
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import java.io.File;
import java.io.IOException;
public class JsonApp {
public static void main(String[] args) throws Exception {
File jsonFile = new File("./src/main/resources/test.json");
ObjectMapper mapper = new ObjectMapper();
System.out.println(mapper.readValue(jsonFile, User.class));
}
}
class PropertyExistsJsonDeserializer extends JsonDeserializer<Boolean> {
#Override
public Boolean deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
p.readValueAsTree(); //consume value
return Boolean.TRUE;
}
}
#JsonIgnoreProperties(ignoreUnknown = true)
#JsonInclude(JsonInclude.Include.NON_NULL)
class User {
private int id;
#JsonDeserialize(using = PropertyExistsJsonDeserializer.class)
#JsonProperty("picture")
private boolean pictureAvailable;
//getters, setters, toString
}
Above code prints:
User{id=123, pictureAvailable=true}

Related

Jackson diffrent name base of other field value

I have POJO class like this
class Data {
#JsonAlias({"username", "name"})
String surname;
Type type;
}
enum Type{
PERSON, USER
}
I want serialization of the Data class but when type is PERSON, JSON property surname is name and when type is USER, surname field as the name
Actually, I can create more child classes of Data class but my real type is more than 10 types with different names, and only difference is name of JSON property and do work similar.
Probably the simplest option would be to use com.fasterxml.jackson.annotation.JsonAnyGetter annotation. Create a method which returns Map<String, String> and create pair which meets your condition. Below code shows how it could look like:
import com.fasterxml.jackson.annotation.JsonAnyGetter;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.json.JsonMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
import java.util.Map;
import java.util.Objects;
public class DataApp {
private final static JsonMapper JSON_MAPPER = JsonMapper.builder()
.enable(SerializationFeature.INDENT_OUTPUT)
.addModule(new JavaTimeModule())
.build();
public static void main(String[] args) throws Exception {
List<Role> roles = List.of(new Role("John", Type.USER), new Role("Tom", Type.PERSON));
JSON_MAPPER.writeValue(System.out, roles);
}
}
#Data
#NoArgsConstructor
#AllArgsConstructor
class Role {
#JsonIgnore
String name;
Type type;
#JsonAnyGetter
private Map<String, String> createDynamicProperties() {
if (Objects.isNull(type)) {
return Map.of();
}
return switch (type) {
case USER -> Map.of("name", name);
case PERSON -> Map.of("surname", name);
};
}
}
enum Type {
PERSON, USER
}
Above code prints:
[ {
"type" : "USER",
"name" : "John"
}, {
"type" : "PERSON",
"surname" : "Tom"
} ]
See also:
How to use dynamic property names for a Json object
Adding a dynamic json property as java pojo for jackson

Jackson xml 2.9.0: #JacksonXmlElementWrapper not working with #JsonCreator & #JsonProperty constructor

I would like that my ParentClass has final fields, 'brokenChildList' list is wrapped xml element and list items have different tag than the list (<brokenChildList><brokenChild/></brokenChildList>).
Here is a snippet of code to reproduce my issues (imports are partially truncated, setters and getters omitted)
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
public class Main {
public static void main(String... args) throws IOException {
ObjectMapper xmlMapper = new XmlMapper();
String xmlString = "<ParentClass><childClass name=\"name1\" value=\"val1\"/><brokenChildList><brokenChild name=\"bc1\" reason=\"bc-val1\"/><brokenChild name=\"bc2\" reason=\"bc-val2\"/></brokenChildList></ParentClass>";
ParentClass parentClass = xmlMapper.readValue(xmlString, ParentClass.class);
StringWriter stringWriter = new StringWriter();
xmlMapper.writeValue(stringWriter, parentClass);
String serialised = stringWriter.toString();
System.out.println(serialised);
System.out.println(xmlString.equals(serialised));
}
public static class ChildClass {
#JacksonXmlProperty(isAttribute = true)
private String name;
#JacksonXmlProperty(isAttribute = true)
private String value;
//getters & setters
}
public static class BrokenChild {
#JacksonXmlProperty(isAttribute = true)
private String name;
#JacksonXmlProperty(isAttribute = true)
private String reason;
//getters & setters
}
public static class ParentClass {
private final ChildClass childClass;
private final List<BrokenChild> brokenChildList;
#JsonCreator
public ParentClass(
#JsonProperty("childClass") ChildClass childClass,
#JsonProperty("brokenChildList") List<BrokenChild> brokenChildList
) {
this.childClass = childClass;
this.brokenChildList = brokenChildList;
}
#JacksonXmlProperty(localName = "childClass")
public ChildClass getChildClass() {
return childClass;
}
#JacksonXmlElementWrapper(localName = "brokenChildList")
#JacksonXmlProperty(localName = "brokenChild")
public List<BrokenChild> getBrokenChildList() {
return brokenChildList;
}
}
}
The above code gives output with Jackson version 2.8.10:
<ParentClass><childClass name="name1" value="val1"/><brokenChildList><brokenChild name="bc1" reason="bc-val1"/><brokenChild name="bc2" reason="bc-val2"/></brokenChildList></ParentClass>
true
With Jackson version 2.9.0 it gives:
Exception in thread "main" com.fasterxml.jackson.databind.JsonMappingException: Duplicate property 'brokenChildList' for [simple type, class org.test.Main$ParentClass]
at [Source: (StringReader); line: 1, column: 1]
I would like to find a solution (and any version after 2.9.0) that will give same output with the attached code.
My failed attempts include:
Replacing #JacksonXmlElementWrapper(localName = "brokenChildList") with #JacksonXmlElementWrapper will rename wrapper element as 'brokenChild' which is undesirable.
Removing #JacksonXmlElementWrapper(localName = "brokenChildList") will rename wrapper element as 'brokenChild' which is undesirable.
This problem is really tricky because Jackson collects metadata from different places: fields, getters, setters, constructor parameters. Also, you can use MixIn but in your case it does not appear.
#JacksonXmlElementWrapper annotation can be attached to FIELD and METHOD type elements and this forces you to declare it on getter. Because ParentClass is immutable and you want to build it with constructor we need to annotate constructor parameters as well. And this is where collision appears: you have a constructor parameter with #JsonProperty("brokenChildList") annotation and getter with #JacksonXmlElementWrapper(localName = "brokenChildList") which reuses the same name. If you would changed localName to #JacksonXmlElementWrapper(localName = "brokenChildListXYZ") (added XYZ) everything would be deserialised and serialised but output would be different then input.
To solve this problem, we can use com.fasterxml.jackson.databind.deser.BeanDeserializerModifier class which allows to filter out fields we do not want to use for deserialisation and which creates collision. Example usage:
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.BeanDescription;
import com.fasterxml.jackson.databind.DeserializationConfig;
import com.fasterxml.jackson.databind.deser.BeanDeserializerModifier;
import com.fasterxml.jackson.databind.introspect.BeanPropertyDefinition;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
import java.io.IOException;
import java.io.StringWriter;
import java.util.List;
import java.util.stream.Collectors;
public class XmlMapperApp {
public static void main(String... args) throws IOException {
SimpleModule module = new SimpleModule();
module.setDeserializerModifier(new BeanDeserializerModifier() {
#Override
public List<BeanPropertyDefinition> updateProperties(DeserializationConfig config, BeanDescription beanDesc, List<BeanPropertyDefinition> propDefs) {
if (beanDesc.getBeanClass() == ParentClass.class) {
return propDefs.stream().filter(p -> p.getConstructorParameter() != null).collect(Collectors.toList());
}
return super.updateProperties(config, beanDesc, propDefs);
}
});
XmlMapper xmlMapper = XmlMapper.xmlBuilder()
.addModule(module)
.build();
//yours code
}
}
To create this example I used version 2.10.0.
See also:
Jackson 2.10 features
Jackson Release 2.10

SpringBoot: Consume & Produce XML with a Custom Serializer + Deserializer

I have a SpringBoot Service with:
Model
public class Payload {
private final String id;
public Payload(String id){
this.id = id;
}
public String getId() {
return this.id;
}
}
Controller
#RestController
#RequestMapping("/payload")
public class PayloadController {
#RequestMapping(method = RequestMethod.POST)
public Payload post(#RequestBody final Payload payload) {
return payload;
}
}
I need this Controller to be able to handle JSON & XML requests and respond with the same format.
This works fine providing I set the Content-Type and Accept headers to the correct media types.
However, my XML payloads need to be in a subtly different structure to my JSON:
XML:
<Payload>
<id value="some-value"/>
</Payload>
JSON:
{
id: "some-value"
}
How do I ensure my id is wrapped in an xml node and has the "value" as an attribute?
I have tried using a #JsonSerialize and #JsonDeserialize annotation on my Payload class but as soon as I do this I get the following error when POSTing XML
{
"timestamp": "2019-10-01T12:06:35.593+0000",
"status": 415,
"error": "Unsupported Media Type",
"message": "Content type 'application/xml;charset=UTF-8' not supported",
"path": "/payload"
}
You need to register 2 converters:
org.springframework.http.converter.json.MappingJackson2HttpMessageConverter for JSON.
org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter for XML.
Because, Payload class fits JSON payload you need to add only JsonCreator and JsonProperty annotations to make it work:
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
public class Payload {
private final String id;
#JsonCreator
public Payload(#JsonProperty(value = "id") String id) {
this.id = id;
}
public String getId() {
return this.id;
}
}
XML payload does not fit by default, so we need to implement custom serialiser:
import com.example.demo.model.Payload;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.dataformat.xml.ser.ToXmlGenerator;
import java.io.IOException;
public class PayloadXmlSerializer extends JsonSerializer<Payload> {
#Override
public void serialize(Payload value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
ToXmlGenerator toXmlGenerator = (ToXmlGenerator) gen;
toXmlGenerator.writeStartObject();
toXmlGenerator.writeObjectFieldStart("id");
toXmlGenerator.setNextIsAttribute(true);
toXmlGenerator.writeFieldName("value");
toXmlGenerator.writeString(value.getId());
toXmlGenerator.setNextIsAttribute(false);
toXmlGenerator.writeEndObject();
toXmlGenerator.writeEndObject();
}
}
and deserialiser:
import com.example.demo.model.Payload;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonPointer;
import com.fasterxml.jackson.core.TreeNode;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.node.TextNode;
import java.io.IOException;
public class PayloadXmlDeserializer extends JsonDeserializer<Payload> {
#Override
public Payload deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
TreeNode root = p.readValueAsTree();
TreeNode value = root.at(JsonPointer.compile("/id/value"));
if (value.isMissingNode()) {
return new Payload(null);
}
TextNode textNode = (TextNode)value;
return new Payload(textNode.textValue());
}
}
Finally, we need to register above HTTP converters and custom serialiser/deserialiser:
import com.example.demo.model.Payload;
import com.example.jackson.PayloadXmlDeserializer;
import com.example.jackson.PayloadXmlSerializer;
import com.fasterxml.jackson.databind.module.SimpleModule;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.List;
#EnableWebMvc
#Configuration
public class WebConfig implements WebMvcConfigurer {
#Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
//JSON
converters.add(new MappingJackson2HttpMessageConverter());
// XML
converters.add(new MappingJackson2XmlHttpMessageConverter(Jackson2ObjectMapperBuilder
.xml()
.modules(payloadModule())
.build()));
}
public SimpleModule payloadModule() {
SimpleModule module = new SimpleModule();
module.addDeserializer(Payload.class, new PayloadXmlDeserializer());
module.addSerializer(Payload.class, new PayloadXmlSerializer());
return module;
}
}
See also:
Using Jackson to add XML attributes to manually-built node-tree
415 Unsupported MediaType for POST request in spring application
Spring MVC

Throw custom exception while deserializing the Date field using jackson in java

DTO:
#Getter
#Setter
#ToString
public class TestDto {
#NotNull
private String id;
#NotNull
#DateTimeFormat(pattern = "YYYY-MM-DD'T'hh:mm:ss.SSSZ")
private Instant timestamp;
}
When I give this input
{"timestamp":"4/23/2018 11:32 PM","id":"132"}
It gives BAD_REQUEST (which it should), but I want to handle this malformed date and throw an exception with my custom exception.
How can I add this?
Since OP requested feature is not supported yet: https://github.com/FasterXML/jackson-annotations/issues/130
Trying to do the same thing with a bit longer approach by using custom deserializer for a field timestamp
Custom exception class:
import com.fasterxml.jackson.core.JsonProcessingException;
public class MyException extends JsonProcessingException {
public MyException(String message) {
super(message);
}
}
Custom Deserializer class:
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.time.Instant;
import java.util.Date;
public class InstantDeserializer extends StdDeserializer<Instant> {
public InstantDeserializer() {
this(null);
}
public InstantDeserializer(Class<?> vc) {
super(vc);
}
private SimpleDateFormat sdf = new SimpleDateFormat("YYYY-MM-DD'T'hh:mm:ss.SSS'Z'");
#Override
public Instant deserialize(JsonParser jp, DeserializationContext ctxt)
throws IOException, JsonProcessingException {
JsonNode node = jp.getCodec().readTree(jp);
Date date = null;
try {
date = sdf.parse(node.asText());
} catch (Exception e) {
throw new MyException("Instant field deserialization failed");
}
return date.toInstant();
}
}
Updated TestDto class:
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.springframework.format.annotation.DateTimeFormat;
import javax.validation.constraints.NotNull;
import java.time.Instant;
#Getter
#Setter
#ToString
public class TestDto {
#NotNull
private String id;
#NotNull
#JsonDeserialize(using = InstantDeserializer.class)
#DateTimeFormat(pattern = "YYYY-MM-DD'T'hh:mm:ss.SSS'Z'")
private Instant timestamp;
}
Invalid Input request:
{"timestamp":"4/23/2018 11:32 PM","id":"132"}
Response:
{
"timestamp": 1552845180271,
"status": 400,
"error": "Bad Request",
"message": "JSON parse error: Instant field deserialization failed; nested exception is com.fasterxml.jackson.databind.JsonMappingException: Instant field deserialization failed (through reference chain: TestDto[\"timestamp\"])"
}
Valid Input Request:
{"timestamp":"2018-04-23T11:32:22.213Z","id":"132"}
Response:
{
"id": "132",
"timestamp": {
"epochSecond": 1514700142,
"nano": 213000000
}
}
If you do not like the way timestamp field is getting deserialized and would like to change that, this SO post will be helpful.

JAX-RS #Produces: how to customize JSON?

I am new to JAX-RS and I want to serve my list of items as JSON. My entity model is something like this:
public class Entity {
private String name;
private Date date;
private Float number;
}
This is how I am invoking the service:
#Path("/entities")
public class EntitiesController {
#GET
#Produces({"application/json"})
public List<Entity> getEntities() {
return EntityDAO.entitiesList();
}
}
However, the date is not formatted but is displayed as a long.
This answer shows how to format a date using a JsonSerializer. If I extend JsonSerializer, then where do I put that subclass in my project?
I figured a solution myself:
Under a new serializers package I created the CustomJsonDateSerializer class, which will be delegated the responsibility of formatting the date attribute thanks to the #JsonSerialize(...) annotation.
So I modified my Entity class adding that annotation ontop of the field:
#JsonSerialize(using = CustomJsonDateSerializer.class)
private Date date;
And this is the content of CustomJsonDateSerializer:
package serializers;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import com.fasterxml.jackson.core.JsonGenerationException;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
public class CustomJsonDateSerializer extends JsonSerializer<Date> {
#Override
public void serialize(Date value, JsonGenerator jgen, SerializerProvider provider)
throws IOException, JsonGenerationException {
SimpleDateFormat formatter = new SimpleDateFormat("dd-MM-yyy");
String format = formatter.format(value);
jgen.writeString(format);
}
}

Categories