Enable Jackson Deserialization of Empty Objects to Null - java

I was asked to change our jackson mapping configuration so that each empty object we deserialize (from JSON) is going to be deserialized as null.
The problem is that I'm struggling to do it, but without any luck. Here is a sample of our ObjectMapper configuration (and example):
ObjectMapper mapper = new ObjectMapper();
mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, true);
mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
JavaTimeModule javaTimeModule = new JavaTimeModule();
javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ISO_DATE_TIME));
javaTimeModule.addDeserializer(Instant.class, InstantDeserializer.INSTANT);
mapper.registerModule(javaTimeModule);
mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
warmupMapper(mapper);
return mapper;
I thought about something like adding:
mapper.configure(
DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT, true);
but it just works on strings.
I'm afraid that using a custom deserializer will not help me, because I'm writing a generic (for all objects) mapper. So I probably need something like a delegator or a post process deserialization method.
So for json like "" or {} I expect to be converted to null in java (and not to empty string or Object instance).

What is a empty object for you? A object with null value fields? A object with no fields? You can create a custom to check the nodes and deserialize how you want. I see no problem to use it in a generic way.
I did a little example:
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.ObjectMapper;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
import com.fasterxml.jackson.databind.module.SimpleModule;
import java.io.IOException;
import java.util.Objects;
public class DeserializerExample<T> extends StdDeserializer<T> {
private final ObjectMapper defaultMapper;
public DeserializerExample(Class<T> clazz) {
super(clazz);
defaultMapper = new ObjectMapper();
}
#Override
public T deserialize(JsonParser jp, DeserializationContext dc) throws IOException, JsonProcessingException {
System.out.println("Deserializing...");
JsonNode node = jp.getCodec().readTree(jp);
for (JsonNode jsonNode : node) {
if (!jsonNode.isNull()) {
return defaultMapper.treeToValue(node, (Class<T>) getValueClass());
}
}
return null;
}
public static void main(String[] args) throws IOException {
ObjectMapper mapper = new ObjectMapper();
SimpleModule module = new SimpleModule();
module.addDeserializer(Person.class, new DeserializerExample(Person.class));
mapper.registerModule(module);
Person person = mapper.readValue("{\"id\":1, \"name\":\"Joseph\"}", Person.class);
Person nullPerson = mapper.readValue("{\"id\":null, \"name\":null}", Person.class);
System.out.println("Is null: " + Objects.isNull(person));
System.out.println("Is null: " + Objects.isNull(nullPerson));
}
}

The only way to do this is to use a custom deserializer:
class CustomDeserializer extends JsonDeserializer<String> {
#Override
public String deserialize(JsonParser jsonParser, DeserializationContext context) throws IOException, JsonProcessingException {
JsonNode node = jsonParser.readValueAsTree();
if (node.asText().isEmpty()) {
return null;
}
return node.toString();
}
}
Then do:
class EventBean {
public Long eventId;
public String title;
#JsonDeserialize(using = CustomDeserializer.class)
public String location;
}
This solution courtesy of Sach141 on this question.

I had the same problem.
I hava a City class and sometimes I recive 'city':{} from a web service request.
So, the standard serializer create a new City with all empty field.
I created a custom deserializer in this way
public class CityJsonDeSerializer extends StdDeserializer<City> {
#Override
public City deserialize(JsonParser jp, DeserializationContext dc) throws IOException, JsonProcessingException {
JsonNode node = jp.getCodec().readTree(jp);
if(node.isNull() || node.asText().isEmpty()|| node.size()==0)
return null;
City city = new City();
... // set all fields
return city;
}
}
The if check the conditions:
'city' : null
'city' : ''
'city' : '{}'
and if it's true, the deserializer returns null.

Another approach is to use a com.fasterxml.jackson.databind.util.Converter<IN,OUT>, which is essentially a postprocessor for deserialization.
Imagine we have a class:
public class Person {
public String id;
public String name;
}
Now imagine we want to deserialize an empty JSON object {} as null, rather than a Person with null values for id and name. We can create the following Converter:
public PersonConverter implements Converter<Person,Person> {
#Override
public Person convert(Person p) {
return isEmpty(p) ? null : value;
}
private static boolean isEmpty(Person p) {
if(p == null) {
return true;
}
if(Optional.ofNullable(p.id).orElse("").isEmpty() &&
Optional.ofNullable(p.name).orElse("").isEmpty()) {
return true;
}
return false;
}
#Override
public JavaType getInputType(TypeFactory typeFactory) {
return typeFactory.constructType(Person.class);
}
#Override
public JavaType getOutputType(TypeFactory typeFactory) {
return typeFactory.constructType(Person.class);
}
}
Note that we have to handle the blank String case because that is (counter-intuitively) the default value assigned to properties not given in JSON, as opposed to null.
Given the converter, we can then annotate our original Person class:
#JsonDeserialize(converter=PersonConverter.class)
public class Person {
public String id;
public String name;
}
The benefit of this approach is that you don't have to think or care about deserialization at all; you're simply deciding what to do with the deserialized object after it's deserialized. And there are many other transformations you can do with a Converter, too. But this works nicely for nullifying "empty" values.

Related

Jackson Custom Deserializer for polymorphic objects and String literals as defaults

I'd like to deserialize an object from YAML with the following properties, using Jackson in a Spring Boot application:
Abstract class Vehicle, implemented by Boat and Car
For simplicity, imagine both have a name, but only Boat has also a seaworthy property, while Car has a top-speed.
mode-of-transport:
type: boat
name: 'SS Boatface'
seaworthy: true
----
mode-of-transport:
type: car`
name: 'KITT'
top-speed: 123
This all works fine in my annotated subclasses using #JsonTypeInfo and #JsonSubTypes!
Now, I'd like to create a shorthand using only a String value, which should create a Car by default with that name:
mode-of-transport: 'KITT'
I tried creating my own custom serializer, but got stuck on most of the relevant details. Please help me fill this in, if this is the right approach:
public class VehicleDeserializer extends StdDeserializer<Merger> {
/* Constructors here */
#Override
public Vehicle deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
if (/* it is an OBJECT */){
// Use the default polymorphic deserializer
} else if (/* it is a STRING */) {
Car car = new Car();
car.setName( /* the String value */ );
return car;
}
return ???; /* what to return here? */
}
}
I found these 2 answers for inspiration, but it looks like combining it with polymorphic types makes it more difficult: How do I call the default deserializer from a custom deserializer in Jackson and Deserialize to String or Object using Jackson
A few things are different than the solutions offered in those questions:
I am processing YAML, not JSON. Not sure about the subtle differences there.
I have no problem hardcoding the 'default' type for Strings inside my Deserializer, hopefully making it simpler.
This was actually easier than I thought to solve it. I got it working using the following:
Custom deserializer implementation:
public class VehicleDeserializer extends StdDeserializer<Vehicle> {
public VehicleDeserializer() {
super(Vehicle.class);
}
#Override
public Vehicle deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException {
if (jp.currentToken() == JsonToken.VALUE_STRING) {
Car car = new Car();
car.setName(jp.readValueAs(String.class));
return car;
}
return jp.readValueAs(Vehicle.class);
}
}
To avoid circular dependencies and to make the custom deserializer work with the polymorphic #JsonTypeInfo and #JsonSubTypes annotations I kept those annotations on the class level of Vehicle, but put the following annotations on the container object I am deserializing:
public class Transport {
#JsonDeserialize(using = VehicleDeserializer.class)
#JsonTypeInfo(use = JsonTypeInfo.Id.NONE)
private Vehicle modeOfTransport;
// Getter, setters
}
This means that by default a Vehicle is deserialized as a polymorphic object, unless explicitly specified to deserialize it using my custom deserializer. This deserializer will then in turn defer to the polymorphism if the input is not a String.
Hopefully this will help someone running into this issue :)
So there is a solution that requires you to handle the jackson errors using a DeserializationProblemHandler (since you want to parse the same type using different inputs, this is not achieved easily using regular means):
public class MyTest {
#Test
public void doTest() throws JsonParseException, JsonMappingException, IOException {
final ObjectMapper om = new ObjectMapper();
om.addHandler(new DeserializationProblemHandler() {
#Override
public Object handleMissingInstantiator(final DeserializationContext ctxt, final Class<?> instClass, final JsonParser p, final String msg) throws IOException {
if (instClass.equals(Car.class)) {
final JsonParser parser = ctxt.getParser();
final String text = parser.getText();
switch (text) {
case "KITT":
return new Car();
}
}
return NOT_HANDLED;
}
#Override
public JavaType handleMissingTypeId(final DeserializationContext ctxt, final JavaType baseType, final TypeIdResolver idResolver, final String failureMsg) throws IOException {
// if (baseType.isTypeOrSubTypeOf(Vehicle.class)) {
final JsonParser parser = ctxt.getParser();
final String text = parser.getText();
switch (text) {
case "KITT":
return TypeFactory.defaultInstance().constructType(Car.class);
}
return super.handleMissingTypeId(ctxt, baseType, idResolver, failureMsg);
}
});
final Container objectValue = om.readValue(getObjectJson(), Container.class);
assertTrue(objectValue.getModeOfTransport() instanceof Car);
final Container stringValue = om.readValue(getStringJson(), Container.class);
assertTrue(stringValue.getModeOfTransport() instanceof Car);
}
private String getObjectJson() {
return "{ \"modeOfTransport\": { \"type\": \"car\", \"name\": \"KITT\", \"speed\": 1}}";
}
private String getStringJson() {
return "{ \"modeOfTransport\": \"KITT\"}";
}
}
class Container {
private Vehicle modeOfTransport;
public Vehicle getModeOfTransport() {
return modeOfTransport;
}
public void setModeOfTransport(final Vehicle modeOfTransport) {
this.modeOfTransport = modeOfTransport;
}
}
#JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type", visible = true)
#JsonSubTypes({
#Type(name = "car", value = Car.class)
})
abstract class Vehicle {
protected String type;
protected String name;
public String getType() {
return type;
}
public void setType(final String type) {
this.type = type;
}
public String getName() {
return name;
}
public void setName(final String name) {
this.name = name;
}
}
#JsonTypeName("car")
class Car extends Vehicle {
private int speed;
public int getSpeed() {
return speed;
}
public void setSpeed(final int speed) {
this.speed = speed;
}
}
Note that I used JSON, not YAML, and you need to add your other subtypes as well.

Deserialize string using custom deserializer specified in field of class

I need to write a method that takes some object, some field name fieldName that exists in the given object's class, and some field value value. The value is the JSON-serialized form of the field. That method shall take the value and deserialize it accordingly, something like this:
static void setField(Object obj, String fieldName, String value) throws Exception {
Field field = obj.getClass().getDeclaredField(fieldName)
Object valObj = objectMapper.readValue(value, field.getType());
field.set(obj, valObj);
}
(I actually only need to retrieve the deserialized value, and not set it again, but this makes it a better example.)
This works, as long as jackson's default deserialization is sufficient. Now let's assume I have a class with a custom (de)serializer:
class SomeDTO {
String foo;
#JsonSerialize(using = CustomInstantSerializer.class)
#JsonDeserialize(using = CustomInstantDeserializer.class)
Instant bar;
}
One possible solution would be to manually check for JsonDeserialize annotations. However, I really do not want to try to replicate whatever policies Jackson follows to decide what serializer to use, as that seems brittle (for example globally registered serializers).
Is there a good way to deserialize the value using the field's deserialization configuration defined in the DTO class? Maybe deserializing the value into the field's type while passing the field's annotations along to Jackson, so they get honored?
I managed to get a hold of an AnnotatedMember instance, which holds all the required information (JSON-annotations and reflective field- or setter/getter-access), but couldn't figure out how I would use it to deserialize a standalone value due to lack of documentation:
final JavaType dtoType = objectMapper.getTypeFactory().constructType(SomeDTO.class);
final BeanDescription description = objectMapper.getDeserializationConfig().introspect(dtoType);
for (BeanPropertyDefinition propDef: beanDescription.findProperties()) {
final AnnotatedMember mutator = propertyDefinition.getNonConstructorMutator();
// now what? Also: How do I filter for the correct property?
}
One possibility would be to serialize the object, replace the given field, and then deserialize it again. This can be easily done when serializing from/to JsonNode instead of JSON-String, like this:
static Object setField(Object obj, String fieldName, String value) throws Exception {
// note: produces a new object instead of modifying the existing one
JsonNode node = objectMapper.valueToTree(obj);
((ObjectNode) node).put(fieldName, value);
return objectMapper.readValue(node.traverse(), obj.getClass());
}
However, serializing and deserializing a whole object just to deserialize a single field seems like a lot of overhead, and might be brittle because other aspects of the DTO class affect the deserialization process of the single field
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
import java.io.IOException;
import java.util.Map;
public final class Jackson {
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper()
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, true);
public static void main(String[] args) throws IOException {
Dto source = makeDto("Master", 31337);
Dto dst = makeDto("Slave", 0xDEADBEEF);
//1. read value of field "fieldName" from json source
//2. clones destination object, sets up field "fieldName" and returns it
//3. in case of no field either on "src" or "dst" - throws an exception
Object result = restoreValue(dst, "details", OBJECT_MAPPER.writeValueAsString(source));
System.out.println(result);
}
private static Object restoreValue(Object targetObject, String fieldName, String sourceObjectAsJson) throws IOException {
String targetObjectAsJson = OBJECT_MAPPER.writeValueAsString(targetObject);
Map sourceAsMap = OBJECT_MAPPER.readValue(sourceObjectAsJson, Map.class);
Map targetAsMap = OBJECT_MAPPER.readValue(targetObjectAsJson, Map.class);
targetAsMap.put(fieldName, sourceAsMap.get(fieldName));
String updatedTargetAsJson = OBJECT_MAPPER.writeValueAsString(targetAsMap);
return OBJECT_MAPPER.readValue(updatedTargetAsJson, targetObject.getClass());
}
private static Dto makeDto(String name, int magic) {
Dto dto = new Dto();
dto.setName(name);
CustomDetails details = new CustomDetails();
details.setMagic(magic);
dto.setDetails(details);
return dto;
}
private static final class Dto {
private String name;
#JsonSerialize(using = CustomDetails.CustomDetailsSerializer.class)
#JsonDeserialize(using = CustomDetails.CustomDetailsDeserializer.class)
private CustomDetails details;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public CustomDetails getDetails() {
return details;
}
public void setDetails(CustomDetails details) {
this.details = details;
}
#Override
public String toString() {
return "Dto{" +
"name='" + name + '\'' +
", details=" + details +
'}';
}
}
private static final class CustomDetails {
private int magic;
public int getMagic() {
return magic;
}
public void setMagic(int magic) {
this.magic = magic;
}
#Override
public String toString() {
return "CustomDetails{" +
"magic=" + magic +
'}';
}
public static final class CustomDetailsSerializer extends StdSerializer<CustomDetails> {
public CustomDetailsSerializer() {
this(null);
}
public CustomDetailsSerializer(Class<CustomDetails> t) {
super(t);
}
#Override
public void serialize(CustomDetails details, JsonGenerator jg, SerializerProvider serializerProvider) throws IOException {
jg.writeStartObject();
jg.writeNumberField("_custom_property_magic", details.magic);
jg.writeEndObject();
}
}
private static final class CustomDetailsDeserializer extends StdDeserializer<CustomDetails> {
public CustomDetailsDeserializer() {
this(null);
}
public CustomDetailsDeserializer(Class<CustomDetails> t) {
super(t);
}
#Override
public CustomDetails deserialize(JsonParser jp, DeserializationContext deserializationContext) throws IOException, JsonProcessingException {
JsonNode node = jp.getCodec().readTree(jp);
int magic = (Integer) node.get("_custom_property_magic").numberValue();
CustomDetails
customDetails = new CustomDetails();
customDetails.setMagic(magic);
return customDetails;
}
}
}
}
so the output is:
Dto{name='Slave', details=CustomDetails{magic=31337}}

java how to override jackson ObjectReader?

I'm writing rest services using jersey and jackson. I have something like this example
import com.mkyong.Track;
#Path("/json/metallica")
public class JSONService {
#GET
#Path("/get")
#Produces(MediaType.APPLICATION_JSON)
public Track getTrackInJSON() {
Track track = new Track();
track.setTitle("Enter Sandman");
track.setSinger("Metallica");
return track;
}
#POST
#Path("/post")
#Consumes(MediaType.APPLICATION_JSON)
public MyResponse createTrackInJSON(Track track) {
return new MyResponse().setResult(true);
}
}
But in my case, the classe Track is not a simple pojo bean.I use a Map to save my data and I create a method to generate json from my object and a constructor to parse json data.
public class JsonObject {
Map<String, String> data = new HashMap<>();
public String toJson(){
return "";
}
}
public class Track extends JsonObject {
public Track(String json) {
//Parse json
// [...]
}
public Track(JsonNode node) {
//Parse node
// [...]
}
public String getTitle() {
if(data.containsKey("title"))
return data.get("title");
return "";
}
public void setTitle(String title) {
data.put("title", title);
}
public String getSinger() {
if(data.containsKey("singer"))
return data.get("singer");
return "";
}
public void setSinger(String singer) {
data.put("singer", singer);
}
#Override
public String toString() {
return "Track [title=" + getTitle() + ", singer=" + getSinger() + "]";
}
public String toJson() {
return "{\"title\": \"" + getTitle() + "\", \"singer\": \"" + getSinger() + "\"}";
}
}
public class MyResponse extends JsonObject {
public boolean getResult() {
if(data.containsKey("result"))
return (boolean) data.get("result");
return false;
}
public MyResponse setResult(boolean value) {
data.put("result", value);
return this;
}
#Override
public String toString() {
return "{\"result\": " + getResult() + "}";
}
}
So my question is: is it possible to create actions before and after the method call to tell to jackson how to generate or parse my object ? using annotation and/or creating an ObjectReader or something like that ?
Thanks
Edit :
Thanks peeskillet.
I'm not sure #JsonAnyGetter et #JsonAnySetter are my solution. I have many objects that extend JsonObject and I want to keep it with getters and setters for my rest api.
So I created a generic JsonSerializer:
public class MyObjectSerializer extends JsonSerializer<JsonObject> {
#Override
public void serialize(JsonObject value, JsonGenerator gen, SerializerProvider serializers)
throws IOException, JsonProcessingException {
gen.writeRaw(value.toJson());
}
}
Then I add this annotation to MyResponse object.
#JsonSerialize(using = MyObjectSerializer.class)
public class MyResponse ...
I wish I did not have to add this annotation in each objects and that was done automatically during rest service return but it works fine and it's not so restrictive.
Now I have another problem with deserialization. I want a generic deserializer calling constructor with parameter JsonNode. But how do I know what class call?
I saw a parameter "as" in #JsonDeserialize annotation.
#JsonDeserialize(using = MyObjectDeserializer.class, as=Track.class)
But I don't find how get this information in the JsonDeserializer. Any idea ?
(Maybe I could open another thread for this question)
I solved my problem.
For the serialization, I created a serializer for my JsonObject I defined in a contextResolver. All classes that extend JsonObject are serialized using this serializer.
public class MyJsonSerializer extends JsonSerializer<JsonObject> {
#Override
public void serialize(JsonObject value, JsonGenerator gen, SerializerProvider serializers)
throws IOException, JsonProcessingException {
gen.writeRaw(value.toJson());
}
}
#Provider
public class JacksonJsonProvider implements ContextResolver<ObjectMapper> {
private static final ObjectMapper MAPPER = new ObjectMapper();
static {
MAPPER.setSerializationInclusion(Include.NON_NULL);
MAPPER.enable(SerializationFeature.INDENT_OUTPUT);
MAPPER.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
}
public JacksonJsonProvider() {
SimpleModule simpleModule = new SimpleModule("SimpleModule", new Version(1,0,0,null, null, null));
simpleModule.addSerializer(JsonObject.class, new MyJsonSerializer());
MAPPER.registerModule(simpleModule);
}
#Override
public ObjectMapper getContext(Class<?> type) {
LOGGER.debug("JacksonProvider.getContext() called with type: "+type);
return MAPPER;
}
}
For deserialization, I use the annotation #JsonCreator() to indicate to jackson what method used to create object.
#JsonCreator()
public JsonObject(JsonNode json) {
super(json);
}
To configure Jackson in JAX-RS, you can register a Context-Resolver<ObjectMapper>, as seen in this post. You can create custom serializers if you need to.
For your specific use case posted above, something as simple at using #JsonAnyGetter would work without doing any other crazy stuff. #JsonAnySetter for deserialization.
public class JsonObject {
Map<String, String> data = new HashMap<>();
#JsonAnyGetter
public Map<String, String> getData() {
return data;
}
#JsonAnySetter
public void put(String field, String value) {
data.put(field, value);
}
public String toJson(){
return "";
}
}
Jackson will serialize all the data in the map, as if they were properties in the class or subclasses. So you don't need to add any properties in the Track class.

Jackson deserialization of type with different objects

I have a result from a web service that returns either a boolean value or a singleton map, e.g.
Boolean result:
{
id: 24428,
rated: false
}
Map result:
{
id: 78,
rated: {
value: 10
}
}
Individually I can map both of these easily, but how do I do it generically?
Basically I want to map it to a class like:
public class Rating {
private int id;
private int rated;
...
public void setRated(?) {
// if value == false, set rated = -1;
// else decode "value" as rated
}
}
All of the polymorphic examples use #JsonTypeInfo to map based on a property in the data, but I don't have that option in this case.
EDIT
The updated section of code:
#JsonProperty("rated")
public void setRating(JsonNode ratedNode) {
JsonNode valueNode = ratedNode.get("value");
// if the node doesn't exist then it's the boolean value
if (valueNode == null) {
// Use a default value
this.rating = -1;
} else {
// Convert the value to an integer
this.rating = valueNode.asInt();
}
}
No no no. You do NOT have to write a custom deserializer. Just use "untyped" mapping first:
public class Response {
public long id;
public Object rated;
}
// OR
public class Response {
public long id;
public JsonNode rated;
}
Response r = mapper.readValue(source, Response.class);
which gives value of Boolean or java.util.Map for "rated" (with first approach); or a JsonNode in second case.
From that, you can either access data as is, or, perhaps more interestingly, convert to actual value:
if (r.rated instanceof Boolean) {
// handle that
} else {
ActualRated actual = mapper.convertValue(r.rated, ActualRated.class);
}
// or, if you used JsonNode, use "mapper.treeToValue(ActualRated.class)
There are other kinds of approaches too -- using creator "ActualRated(boolean)", to let instance constructed either from POJO, or from scalar. But I think above should work.
You have to write your own deserializer. It could look like this:
#SuppressWarnings("unchecked")
class RatingJsonDeserializer extends JsonDeserializer<Rating> {
#Override
public Rating deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {
Map<String, Object> map = jp.readValueAs(Map.class);
Rating rating = new Rating();
rating.setId(getInt(map, "id"));
rating.setRated(getRated(map));
return rating;
}
private int getInt(Map<String, Object> map, String propertyName) {
Object object = map.get(propertyName);
if (object instanceof Number) {
return ((Number) object).intValue();
}
return 0;
}
private int getRated(Map<String, Object> map) {
Object object = map.get("rated");
if (object instanceof Boolean) {
if (((Boolean) object).booleanValue()) {
return 0; // or throw exception
}
return -1;
}
if (object instanceof Map) {
return getInt(((Map<String, Object>) object), "value");
}
return 0;
}
}
Now you have to tell Jackson to use this deserializer for Rating class:
#JsonDeserialize(using = RatingJsonDeserializer.class)
class Rating {
...
}
Simple usage:
ObjectMapper objectMapper = new ObjectMapper();
System.out.println(objectMapper.readValue(json, Rating.class));
Above program prints:
Rating [id=78, rated=10]
for JSON:
{
"id": 78,
"rated": {
"value": 10
}
}
and prints:
Rating [id=78, rated=-1]
for JSON:
{
"id": 78,
"rated": false
}
I found a nice article on the subject: http://programmerbruce.blogspot.com/2011/05/deserialize-json-with-jackson-into.html
I think that the approach of parsing into object, is possibly problematic, because when you send it, you send a string. I am not sure it is an actual issue, but it sounds like some possible unexpected behavior.
example 5 and 6 show that you can use inheritance for this.
Example:
Example 6: Simple Deserialization Without Type Element To Container Object With Polymorphic Collection
Some real-world JSON APIs have polymorphic type members, but don't include type elements (unlike the JSON in the previous examples). Deserializing such sources into polymorphic collections is a bit more involved. Following is one relatively simple solution. (This example includes subsequent serialization of the deserialized Java structure back to input JSON, but the serialization is relatively uninteresting.)
// input and output:
// {
// "animals":
// [
// {"name":"Spike","breed":"mutt","leash_color":"red"},
// {"name":"Fluffy","favorite_toy":"spider ring"},
// {"name":"Baldy","wing_span":"6 feet",
// "preferred_food":"wild salmon"}
// ]
// }
import java.io.File;
import java.io.IOException;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import org.codehaus.jackson.JsonNode;
import org.codehaus.jackson.JsonParser;
import org.codehaus.jackson.JsonProcessingException;
import org.codehaus.jackson.Version;
import org.codehaus.jackson.map.DeserializationContext;
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.map.deser.StdDeserializer;
import org.codehaus.jackson.map.module.SimpleModule;
import org.codehaus.jackson.node.ObjectNode;
import fubar.CamelCaseNamingStrategy;
public class Foo
{
public static void main(String[] args) throws Exception
{
AnimalDeserializer deserializer =
new AnimalDeserializer();
deserializer.registerAnimal("leash_color", Dog.class);
deserializer.registerAnimal("favorite_toy", Cat.class);
deserializer.registerAnimal("wing_span", Bird.class);
SimpleModule module =
new SimpleModule("PolymorphicAnimalDeserializerModule",
new Version(1, 0, 0, null));
module.addDeserializer(Animal.class, deserializer);
ObjectMapper mapper = new ObjectMapper();
mapper.setPropertyNamingStrategy(
new CamelCaseNamingStrategy());
mapper.registerModule(module);
Zoo zoo =
mapper.readValue(new File("input_6.json"), Zoo.class);
System.out.println(mapper.writeValueAsString(zoo));
}
}
class AnimalDeserializer extends StdDeserializer<Animal>
{
private Map<String, Class<? extends Animal>> registry =
new HashMap<String, Class<? extends Animal>>();
AnimalDeserializer()
{
super(Animal.class);
}
void registerAnimal(String uniqueAttribute,
Class<? extends Animal> animalClass)
{
registry.put(uniqueAttribute, animalClass);
}
#Override
public Animal deserialize(
JsonParser jp, DeserializationContext ctxt)
throws IOException, JsonProcessingException
{
ObjectMapper mapper = (ObjectMapper) jp.getCodec();
ObjectNode root = (ObjectNode) mapper.readTree(jp);
Class<? extends Animal> animalClass = null;
Iterator<Entry<String, JsonNode>> elementsIterator =
root.getFields();
while (elementsIterator.hasNext())
{
Entry<String, JsonNode> element=elementsIterator.next();
String name = element.getKey();
if (registry.containsKey(name))
{
animalClass = registry.get(name);
break;
}
}
if (animalClass == null) return null;
return mapper.readValue(root, animalClass);
}
}
class Zoo
{
public Collection<Animal> animals;
}
abstract class Animal
{
public String name;
}
class Dog extends Animal
{
public String breed;
public String leashColor;
}
class Cat extends Animal
{
public String favoriteToy;
}
class Bird extends Animal
{
public String wingSpan;
public String preferredFood;
}
I asked a similar question - JSON POJO consumer of polymorphic objects
You have to write your own deserialiser that gets a look-in during the deserialise process and decides what to do depending on the data.
There may be other easier methods but this method worked well for me.

How can I include raw JSON in an object using Jackson?

I am trying to include raw JSON inside a Java object when the object is (de)serialized using Jackson. In order to test this functionality, I wrote the following test:
public static class Pojo {
public String foo;
#JsonRawValue
public String bar;
}
#Test
public void test() throws JsonGenerationException, JsonMappingException, IOException {
String foo = "one";
String bar = "{\"A\":false}";
Pojo pojo = new Pojo();
pojo.foo = foo;
pojo.bar = bar;
String json = "{\"foo\":\"" + foo + "\",\"bar\":" + bar + "}";
ObjectMapper objectMapper = new ObjectMapper();
String output = objectMapper.writeValueAsString(pojo);
System.out.println(output);
assertEquals(json, output);
Pojo deserialized = objectMapper.readValue(output, Pojo.class);
assertEquals(foo, deserialized.foo);
assertEquals(bar, deserialized.bar);
}
The code outputs the following line:
{"foo":"one","bar":{"A":false}}
The JSON is exactly how I want things to look. Unfortunately, the code fails with an exception when attempting to read the JSON back in to the object. Here is the exception:
org.codehaus.jackson.map.JsonMappingException: Can not deserialize instance of java.lang.String out of START_OBJECT token
at [Source: java.io.StringReader#d70d7a; line: 1, column: 13] (through reference chain: com.tnal.prism.cobalt.gather.testing.Pojo["bar"])
Why does Jackson function just fine in one direction but fail when going the other direction? It seems like it should be able to take its own output as input again. I know what I'm trying to do is unorthodox (the general advice is to create an inner object for bar that has a property named A), but I don't want to interact with this JSON at all. My code is acting as a pass-through for this code -- I want to take in this JSON and send it back out again without touching a thing, because when the JSON changes I don't want my code to need modifications.
Thanks for the advice.
EDIT: Made Pojo a static class, which was causing a different error.
#JsonRawValue is intended for serialization-side only, since the reverse direction is a bit trickier to handle. In effect it was added to allow injecting pre-encoded content.
I guess it would be possible to add support for reverse, although that would be quite awkward: content will have to be parsed, and then re-written back to "raw" form, which may or may not be the same (since character quoting may differ).
This for general case. But perhaps it would make sense for some subset of problems.
But I think a work-around for your specific case would be to specify type as 'java.lang.Object', since this should work ok: for serialization, String will be output as is, and for deserialization, it will be deserialized as a Map. Actually you might want to have separate getter/setter if so; getter would return String for serialization (and needs #JsonRawValue); and setter would take either Map or Object. You could re-encode it to a String if that makes sense.
Following #StaxMan answer, I've made the following works like a charm:
public class Pojo {
Object json;
#JsonRawValue
public String getJson() {
// default raw value: null or "[]"
return json == null ? null : json.toString();
}
public void setJson(JsonNode node) {
this.json = node;
}
}
And, to be faithful to the initial question, here is the working test:
public class PojoTest {
ObjectMapper mapper = new ObjectMapper();
#Test
public void test() throws IOException {
Pojo pojo = new Pojo("{\"foo\":18}");
String output = mapper.writeValueAsString(pojo);
assertThat(output).isEqualTo("{\"json\":{\"foo\":18}}");
Pojo deserialized = mapper.readValue(output, Pojo.class);
assertThat(deserialized.json.toString()).isEqualTo("{\"foo\":18}");
// deserialized.json == {"foo":18}
}
}
I was able to do this with a custom deserializer (cut and pasted from here)
package etc;
import java.io.IOException;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.TreeNode;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
/**
* Keeps json value as json, does not try to deserialize it
* #author roytruelove
*
*/
public class KeepAsJsonDeserializer extends JsonDeserializer<String> {
#Override
public String deserialize(JsonParser jp, DeserializationContext ctxt)
throws IOException {
TreeNode tree = jp.getCodec().readTree(jp);
return tree.toString();
}
}
Use it by annotating the desired member like this:
#JsonDeserialize(using = KeepAsJsonDeserializer.class)
private String value;
#JsonSetter may help. See my sample ('data' is supposed to contain unparsed JSON):
class Purchase
{
String data;
#JsonProperty("signature")
String signature;
#JsonSetter("data")
void setData(JsonNode data)
{
this.data = data.toString();
}
}
This is a problem with your inner classes. The Pojo class is a non-static inner class of your test class, and Jackson cannot instantiate that class. So it can serialize, but not deserialize.
Redefine your class like this:
public static class Pojo {
public String foo;
#JsonRawValue
public String bar;
}
Note the addition of static
Adding to Roy Truelove's great answer, this is how to inject the custom deserialiser in response to appearance of #JsonRawValue:
import com.fasterxml.jackson.databind.Module;
#Component
public class ModuleImpl extends Module {
#Override
public void setupModule(SetupContext context) {
context.addBeanDeserializerModifier(new BeanDeserializerModifierImpl());
}
}
import java.util.Iterator;
import com.fasterxml.jackson.annotation.JsonRawValue;
import com.fasterxml.jackson.databind.BeanDescription;
import com.fasterxml.jackson.databind.DeserializationConfig;
import com.fasterxml.jackson.databind.deser.BeanDeserializerBuilder;
import com.fasterxml.jackson.databind.deser.BeanDeserializerModifier;
import com.fasterxml.jackson.databind.deser.SettableBeanProperty;
public class BeanDeserializerModifierImpl extends BeanDeserializerModifier {
#Override
public BeanDeserializerBuilder updateBuilder(DeserializationConfig config, BeanDescription beanDesc, BeanDeserializerBuilder builder) {
Iterator<SettableBeanProperty> it = builder.getProperties();
while (it.hasNext()) {
SettableBeanProperty p = it.next();
if (p.getAnnotation(JsonRawValue.class) != null) {
builder.addOrReplaceProperty(p.withValueDeserializer(KeepAsJsonDeserialzier.INSTANCE), true);
}
}
return builder;
}
}
This easy solution worked for me:
public class MyObject {
private Object rawJsonValue;
public Object getRawJsonValue() {
return rawJsonValue;
}
public void setRawJsonValue(Object rawJsonValue) {
this.rawJsonValue = rawJsonValue;
}
}
So I was able to store raw value of JSON in rawJsonValue variable and then it was no problem to deserialize it (as object) with other fields back to JSON and send via my REST. Using #JsonRawValue didnt helped me because stored JSON was deserialized as String, not as object, and that was not what I wanted.
This even works in a JPA entity:
private String json;
#JsonRawValue
public String getJson() {
return json;
}
public void setJson(final String json) {
this.json = json;
}
#JsonProperty(value = "json")
public void setJsonRaw(JsonNode jsonNode) {
// this leads to non-standard json, see discussion:
// setJson(jsonNode.toString());
StringWriter stringWriter = new StringWriter();
ObjectMapper objectMapper = new ObjectMapper();
JsonGenerator generator =
new JsonFactory(objectMapper).createGenerator(stringWriter);
generator.writeTree(n);
setJson(stringWriter.toString());
}
Ideally the ObjectMapper and even JsonFactory are from the context and are configured so as to handle your JSON correctly (standard or with non-standard values like 'Infinity' floats for example).
Here is a full working example of how to use Jackson modules to make #JsonRawValue work both ways (serialization and deserialization):
public class JsonRawValueDeserializerModule extends SimpleModule {
public JsonRawValueDeserializerModule() {
setDeserializerModifier(new JsonRawValueDeserializerModifier());
}
private static class JsonRawValueDeserializerModifier extends BeanDeserializerModifier {
#Override
public BeanDeserializerBuilder updateBuilder(DeserializationConfig config, BeanDescription beanDesc, BeanDeserializerBuilder builder) {
builder.getProperties().forEachRemaining(property -> {
if (property.getAnnotation(JsonRawValue.class) != null) {
builder.addOrReplaceProperty(property.withValueDeserializer(JsonRawValueDeserializer.INSTANCE), true);
}
});
return builder;
}
}
private static class JsonRawValueDeserializer extends JsonDeserializer<String> {
private static final JsonDeserializer<String> INSTANCE = new JsonRawValueDeserializer();
#Override
public String deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
return p.readValueAsTree().toString();
}
}
}
Then you can register the module after creating the ObjectMapper:
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModule(new JsonRawValueDeserializerModule());
String json = "{\"foo\":\"one\",\"bar\":{\"A\":false}}";
Pojo deserialized = objectMapper.readValue(json, Pojo.class);
I had the exact same issue.
I found the solution in this post :
Parse JSON tree to plain class using Jackson or its alternatives
Check out the last answer.
By defining a custom setter for the property that takes a JsonNode as parameter and calls the toString method on the jsonNode to set the String property, it all works out.
Using an object works fine both ways... This method has a bit of overhead deserializing the raw value in two times.
ObjectMapper mapper = new ObjectMapper();
RawJsonValue value = new RawJsonValue();
value.setRawValue(new RawHello(){{this.data = "universe...";}});
String json = mapper.writeValueAsString(value);
System.out.println(json);
RawJsonValue result = mapper.readValue(json, RawJsonValue.class);
json = mapper.writeValueAsString(result.getRawValue());
System.out.println(json);
RawHello hello = mapper.readValue(json, RawHello.class);
System.out.println(hello.data);
RawHello.java
public class RawHello {
public String data;
}
RawJsonValue.java
public class RawJsonValue {
private Object rawValue;
public Object getRawValue() {
return rawValue;
}
public void setRawValue(Object value) {
this.rawValue = value;
}
}
I had a similar problem, but using a list with a lot of JSON itens (List<String>).
public class Errors {
private Integer status;
private List<String> jsons;
}
I managed the serialization using the #JsonRawValue annotation. But for deserialization I had to create a custom deserializer based on Roy's suggestion.
public class Errors {
private Integer status;
#JsonRawValue
#JsonDeserialize(using = JsonListPassThroughDeserialzier.class)
private List<String> jsons;
}
Below you can see my "List" deserializer.
public class JsonListPassThroughDeserializer extends JsonDeserializer<List<String>> {
#Override
public List<String> deserialize(JsonParser jp, DeserializationContext cxt) throws IOException, JsonProcessingException {
if (jp.getCurrentToken() == JsonToken.START_ARRAY) {
final List<String> list = new ArrayList<>();
while (jp.nextToken() != JsonToken.END_ARRAY) {
list.add(jp.getCodec().readTree(jp).toString());
}
return list;
}
throw cxt.instantiationException(List.class, "Expected Json list");
}
}

Categories

Resources