In a custom Jackson deserialiser, is there a way to delegate certain properties back to the default deserialiser?
#Override
public final T deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {
ObjectNode node = jp.getCodec().readTree(jp);
T type = createType();
//custom deserialise some fields here
...
// Is there a way to delegate everything else back to Jackson?
ObjectNode nodeToDelegate = node.get("someField");
// delegate back to jackson and deserialise into `type`
// nodeToDelegate can be anything - Number / Object / Array / etc.
}
p.s. I do need custom deserialiser and cannot use type annotations.
You can use following code to achieve this.
JsonParser parser = nodeToDelegate.traverse();
parser.setCodec(jp.getCodec());
parser.readValueAs(<Type>.class);
Related
I have found how to customize ObjectMapper date format in order to let Spring to help to auto serialize/deserialize (serialize when I want to return object to client, deserialize when the request body is json object), but I have lot of DTO with different date format, some might need yyyy-mm-dd, some is dd-mm-yyyy, one ObjectMapper will not work for different required date format, what is the best practice solution for this issue?
#Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
super.configureMessageConverters(converters);
converters.add(mappingJacksonHttpMessageConverter());
}
MappingJacksonHttpMessageConverter mappingJacksonHttpMessageConverter() {
MappingJacksonHttpMessageConverter mappingJacksonHttpMessageConverter = new MappingJacksonHttpMessageConverter();
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setDateFormat(new SimpleDateFormat("dd-MM-yyyy"));
objectMapper.setSerializationInclusion(JsonSerialize.Inclusion.NON_NULL);
mappingJacksonHttpMessageConverter.setObjectMapper(objectMapper);
mappingJacksonHttpMessageConverter.setPrettyPrint(true);
return mappingJacksonHttpMessageConverter;
}
You could use custom Serializers and handle the different formats within a single Serializer. Here are a few pages that have some info on how to create custom Serializer/Deserializers:
Create Custom Serializer
Create Custom Deserializer
-- Edit --
From the documentation for MappingJacksonHttpMessageConverter (some emphasis added):
setObjectMapper
public void setObjectMapper(org.codehaus.jackson.map.ObjectMapper objectMapper)
Set the ObjectMapper for this view. If not set, a default ObjectMapper is used.
Setting a custom-configured ObjectMapper is one way to take further control
of the JSON serialization process. For example, an extended SerializerFactory
can be configured that provides custom serializers for specific types.
The other option for refining the serialization process is to use Jackson's
provided annotations on the types to be serialized, in which case a
custom-configured ObjectMapper is unnecessary.
This means that you do not even need to call setObjectMapper if you have Serializers/Deserializers defined by annotations (as described in the links I posted above). For your benefit, here is an example:
For Serializing:
Create a StdSerializer object to handle the type you are interested in
public class ItemSerializer extends StdSerializer<Item> {
// ...
#Override
public void serialize(Item value, JsonGenerator jgen, SerializerProvider provider) {
// Write the Item data into the JsonGenerator
}
}
Define the Serializer for the object via annotations
#JsonSerialize(using = ItemSerializer.class)
public class Item {
// ...
}
For Deserialization
Create a StdDeserializer object to handle the type you are interested in
public class ItemDeserializer extends StdDeserializer<Item> {
// ...
#Override
public Item deserialize(JsonParser jp, DeserializationContext ctxt)
throws IOException, JsonProcessingException {
// Handle the different date formats here!
return new Item(/*parsed date object*/);
}
}
Define the Deserializer for the object via annotations
#JsonDeserialize(using = ItemDeserializer.class)
public class Item {
// ...
}
In a custom Deserializer for an Entity I want to call the deserialization of (some of) the fields recursively in a way, that #JsonFormat annotations are respected. More concretely this is how the Deserializer looks like:
public class DealPatchDeserializer extends JsonDeserializer<DealPatch> {
public DealPatch deserialize(JsonParser p, DeserializationContext ctxt)
throws IOException, JsonProcessingException {
JsonNode root = p.readValueAsTree();
JsonNode redeemableFromNode = root.path("redeemableFrom");
JsonParser redeemableFromParser = redeemableFromNode.traverse(p.getCodec());
Date date = redeemableFromParser.readValueAs(Date.class);
}
In that example the JsonParser.readValueAs() does not take the annotated
#JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "dd.MM.yyyy")
private Date redeemableTo;
pattern into account, and thus generates an exception because of the unknown (German) date format.
Is there any way to deserialize fields of an object so that it respects the annotated formats?
If the entire redeemableFrom object does not need custom deserialization, you can parse it as a regular Java object, with the annotations, by calling treeToValue(). Something like this should work:
p.getCodec().treeToValue(redeemableFromNode, RedeemableFrom.class);
I have written a custom deserializer to map only the required fields using jackson. Here goes.
public class GeneralDeserializer extends JsonDeserializer<GeneralDomain> {
#Override
public GeneralDomain deserialize(JsonParser jp, DeserializationContext ctxt)
throws IOException, JsonProcessingException {
final JsonNode jsonNode = jp.getCodec().readTree(jp);
final Map<String, String> map = new ObjectMapper().convertValue(jsonNode, Map.class);
final String event = "Proxy";
return new GeneralDomain(map.get("id"), event, map.get("name"), map.get("lastLogin"));
}
#Override
public Class<GeneralDomain> handledType() {
return GeneralDomain.class;
}
}
I have a mixin class too for this to add extra annotations.
#JsonDeserialize(using = GeneralDeserializer.class)
public class GeneralDomainMixIn{}
I fetch the object in this way,
ObjectMapper mapper = new ObjectMapper();
mapper.addMixIn(GeneralDomain.class, SimpleRevealPublicEventMixIn.class);
String json = "{\"id\": 111, \"name\": David, \"lastLogin\": \"02-10-2016 10:32:00 AM\"}";
GeneralDomain readValue = mapper.readValue(json, GeneralDomain.class);
This works great. But as you can see in the custom deserializer, I am hard coding the event field value. This will be passed on by some other instance variable in the main class. I have to pass this field to the custom deserializer. So is there a way to access this variable inside the deserializer? Or is there any other alternative way to achieve this? Please help me out. Thanks.
Got the answer finally. Thanks to Philip in this link.
All I had to do was this.
Create an instance of InjectableValues
private InjectableValues injectEventType() {
return new InjectableValues.Std()
.addValue("event", "proxy")
}
use this method to set the injectEventType method in mapper class
GeneralDomain readValue = mapper.setInjectableValues(injectEventType()).readValue(json, GeneralDomain.class);
In my deserialize method I had to retrieve the values provided by the InjectableValues:
String event = String.valueOf(ctxt.findInjectableValue("event", null, null));
I need to create a general deserializer; in other words I don't know what the deserialised target class will be.
I have seen examples on the internet where by they create a deserializer such as JsonDeserializer<Customer> and then return a new Customer(...) at the end. The problem is that I don't know what the return class will be.
I imagine I will need to use reflection to create an instance of the class and populate the field. How can I do it from the deserialize method?
public class JsonApiDeserializer extends JsonDeserializer<Object> {
#Override
public Object deserialize(JsonParser jp, DeserializationContext ctxt)
throws IOException, JsonProcessingException {
//Need to parse the JSON and return a new instance here
}
}
After some tests, I find #jax 's answer has a problem.
As #Staxman pointed out, createContextual() is called during construction of Deserializer, not in every process of deserialization. And the deserializer returned by createContextual will be cached by the Jackson library. So if your deserializer is used with more than 1 type(such as sub types of a common parent), it will throw out type mismatch exception, cause the targetClass property will be the last type cached by the Jackson library.
The correct solution should be:
public class JsonApiDeserializer extends JsonDeserializer<Object> implements
ContextualDeserializer {
private Class<?> targetClass;
public JsonApiDeserializer() {
}
public JsonApiDeserializer(Class<?> targetClass) {
this.targetClass = targetClass;
}
#Override
public Object deserialize(JsonParser p, DeserializationContext ctxt)
throws IOException, JsonProcessingException {
Object clazz = targetClass.newInstance();
//Now I have an instance of the annotated class I can populate the fields via reflection
return clazz;
}
#Override
public JsonDeserializer<?> createContextual(DeserializationContext ctxt,
BeanProperty property) throws JsonMappingException {
//gets the class type of the annotated class
targetClass = ctxt.getContextualType().getRawClass();
//this new JsonApiDeserializer will be cached
return new JsonApiDeserializer(targetClass);
}
}
Essentially, there are only 2 cases you need to cater for, Object and Object[], for which you can always deserialize to:
A Map
An array of Map
Something like this should work:
public class JsonApiDeserializer extends JsonDeserializer<Object> {
#Override
public Object deserialize(JsonParser jp, DeserializationContext ctxt)
throws IOException, JsonProcessingException {
String text = jp.getText();
if (text.startsWith("{"))
return new ObjectMapper().readValue(text, Map.class);
return new ObjectMapper().readValue(text, Map[].class);
}
}
Disclaimer: Uncompiled and untested
I got it working using ContextualDeserializer
public class JsonApiDeserializer extends JsonDeserializer<Object> implements
ContextualDeserializer {
private Class<?> targetClass;
#SneakyThrows
#Override
public Object deserialize(JsonParser p, DeserializationContext ctxt)
throws IOException, JsonProcessingException {
Object clazz = targetClass.newInstance();
//Now I have an instance of the annotated class I can populate the fields via reflection
return clazz;
}
#Override
public JsonDeserializer<?> createContextual(DeserializationContext ctxt,
BeanProperty property) throws JsonMappingException {
//gets the class type of the annotated class
targetClass = ctxt.getContextualType().getRawClass();
return this;
}
}
I am still a little unsure of why this works as I already have a DeserializationContext ctxt in the original deserialize method but it returns null when I do ctxt.getContextualType().
Can someone explain?
If you know the message structure in advance, you can use this tool to easily generate POJOs from a given JSON string.
However, if your message format changes during runtime, and there is no other information for you to determine the type (for example, header information) you can deserialize into a Map and process the fields manually.
For example, with Jackson:
ObjectMapper mapper = new ObjectMapper();
Map<String, Object> userData = mapper.readValue(jsonData, Map.class);
I am not sure I completely got your question right but what you can do is to inspect the properties of the json inside the deserialiser doing something like:
ObjectMapper objectMapper = (ObjectMapper) jp.getCodec();
ObjectNode node = objectMapper.readTree(jp);
and then node.has("propertyName") so that you can create, setup and return your object and leave to the client of the deserialiser the responsibility of the cast.
you say " in other words I don't know what the deserialised target class will be. "
so I don't get if you can at least infer that, more info would be helpful
If you know which classes can be deserialized in compile-time, but need to dynamically choose the right one in runtime depending on JSON contents I can suggest the following.
Add some classifier field into the JSON. This field will help your code know how to deal with the following data. As far as I can see, you already have the "type" field so that can be used.
Introduce a factory that will instantiate the specific classes depending on the input from JSON. For example, it may have the method like Object create(string typeFromJson, Map data). Such factory may populate the newly-created object with the data as well.
If this is not the case and you don't know your required interfaces already, you are in trouble. It can be somewhat be worked around in C# with the use of dynamic keyword, but Java doesn't have such a feature yet.
Also, AFAIK, there is a way in Jackson to specify classes that need to be automatically deserialized and injected into #Post method calls in your REST resource class.
I have a POJO
class Product {
String name;
Size size;
}
So, I want to map a deserialize a JSON to my POJO. If I have both the attributes in my JSON, it is not a problem.
But in my case, sometimes size will not be a part of the JSON. There might be a third attribute 'type' based on which I will set my size. I do not want to include 'type' in my POJO. Are there any Jackson annotations which can do this?
write your custom Deserializers:
SimpleModule module =
new SimpleModule("ProductDeserializerModule",
new Version(1, 0, 0, null));
module.addDeserializer(Product.class, new ProductJsonDeserializer());
mapper = new ObjectMapper();
mapper.registerModule(module);
//...
class ProductJsonDeserializer extends JsonDeserializer<Product>
{
#Override
public Product deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException
{
// handle here if exist a third attribute 'type' and create the product
}
}
More info here: http://wiki.fasterxml.com/JacksonHowToCustomDeserializers
Found a pretty simple solution for this!
When a JSON attribute is attempted to be mapped to my POJO's attribute, it just checks whether a setter exists for it.
For example, if there is an attribute type in JSON, it will try to hit a method named setType(obj) in my POJO, regardless of whether there exists an attribute named type.
This worked for me! I simply set my other attributes inside this setter.