I'm using Jackson to deserialize some JSON and I've run into some trouble while trying to use a custom deserializer for one of the fields.
class MyClass
{
private static class SpecialPropertyDeserializer extends JsonDeserializer<SpecialProperty>
{
#Override
public SpecialProperty deserialize(JsonParser jsonParser,
DeserializationContext deserializationContext) throws IOException, JsonProcessingException
{
// do some custom deserialisation
}
}
private static class SpecialProperty
{
private String m_foo;
private String m_bar;
#JsonCreator
SpecialProperty(#JsonProperty("foo") String foo,
#JsonProperty("bar") String bar)
{
m_foo = foo;
m_bar = bar;
}
}
private String m_identifier;
private String m_version;
#JsonDeserialize(using = SpecialPropertyDeseializer.class)
private SpecialProperty m_specialProperty;
#JsonCreator
MyClass(#JsonProperty("identifier") String identifier,
#JsonProperty("version") String version,
#JsonProperty("specialProperty") SpecialProperty specialProperty)
{
m_identifier = identifier;
m_version = version;
m_specialProperty = specialProperty;
}
}
and this is the JSON I want to deserialize:
{
"identifier" : "some-id",
"version" : "1.7",
"specialProperty" : {
"foo" : "str1",
"bar" : "str2"
},
}
I invoke the mapper as follows:
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, true);
return objectMapper.readValue(input, MyClass.class);
I've observed the following behaviour:
Without a special property it all works fine - i.e. remove all
references to SpecialProperty from the code and the JSON.
If I include SpecialProperty in the JSON but remove the custom
deserializer for it then it also works fine. The ctor for
SpecialProperty is called.
With the custom deserializer it doesn't work. The ctor for SpecialProperty is called but the custom deserializer is not.
What am I doing wrong?
#JsonDeserialize annotation can be placed on a field, a setter or a class. Jackson will take it into account if what is annotated is what it uses to set the value.
E.g.1 It will notice #JsonDeserialize over a setter if it uses the setter to set the value of a field.
E.g.2 It will notice #JsonDeserialize over a field if it directly sets this field without using a setter or a constructor.
It will tend to take it into account if it's on a class unless it's overridden by a more specific annotation on a field or setter docs. I reckon the docs could be clearer on the above details.
In your case you have the annotation over the SpecialProperty field but you are setting this field in the MyClass constructor so it's ignored.
In this case you can move #JsonDeserialize over the class instead of over the field. That's probably the simplest solution in your case. E.g.
#JsonDeserialize(using = MyClass.SpecialPropertyDeserializer.class)
private static class SpecialProperty {
Or you can skip the annotation altogether and register the deserializer on the mapper. First make SpecialProperty and SpecialPropertyDeserializer non private in MyClass and then:
ObjectMapper objectMapper = new ObjectMapper();
SimpleModule module = new SimpleModule();
module.addDeserializer(MyClass.SpecialProperty.class, new MyClass.SpecialPropertyDeserializer());
objectMapper.registerModule(module);
You can also get rid of constructor of MyClass and the current annotation over the SpecialProperty field will be taken into account.
Related
I wrote a Custom Serializer and Custom Deserializer to serialize properties marked with #Confidential annotation.
#Data
public class Person {
private String name;
#Confidential
private String address;
}
The Custom Serializer serializes a POJO with following values:
{ "name": "John Doe", "address": "Kearney St"}
as follows:
{"name":"John Doe", "address": {"value":"IjIwMzEwIDU4dGggTG4gTkUi"}}
The Custom Deserializer is also able to deserialize the JSON back to the Person POJO fine.
However, when I make the fields in the Person POJO final, serialization continues to work, but deserialization fails.
#Data
public class Person {
private final String name;
#Confidential
private final String address;
}
Here's the BeanSerializerModifier implementation:
#AllArgsConstructor
public class CustomDeserializerModifier extends BeanDeserializerModifier {
private final ObjectMapper objectMapper;
#Override
public BeanDeserializerBuilder updateBuilder(final DeserializationConfig config,
final BeanDescription beanDesc,
final BeanDeserializerBuilder builder) {
Iterator<SettableBeanProperty> beanPropertyIterator = builder.getProperties();
beanPropertyIterator.forEachRemaining(settableBeanProperty -> {
final Confidential annotation = settableBeanProperty.getAnnotation(Confidential.class);
if (encryptedProperty != null) {
JsonDeserializer<Object> current = settableBeanProperty.getValueDeserializer();
final SettableBeanProperty newSettableBeanProperty =
settableBeanProperty.withValueDeserializer(
new CustomDeserializer(annotation, current, objectMapper)
);
builder.addOrReplaceProperty(newSettableBeanProperty, true);
}
});
return builder;
}
}
I found that CustomDeserializer, never gets called when the Person POJO fields are final.
Here's the error message:
com.fasterxml.jackson.databind.JsonMappingException: Can not deserialize instance of java.lang.String out of START_OBJECT token
at [Source: {"name":"John Doe","address":{"value":"IjIwMzEwIDU4dGggTG4gTkUi"}}; line: 1, column: 30] (through reference chain: com.custom.model.Person["address"])
Can a Jackson expert please tell me why my CustomDeserializer isn't getting invoked when the POJO fields are final.
Thank you!
As mentioned, serialization will perfectly work for both mutable and immutable fields. Deserialization issue will only occur when using immutable fields as the BeanDeserializerModifier won't work in such a case.
In the Jackson terminology, immutable fields are named creator properties, meaning they are initialized using creators. See BeanDeserializerBase#resolve.
To correctly handle this use case, the ObjectMapper may be created with a custom DeserializationContext (an extended implementation of the ObjectMapper may also set the protected field related to the deserialization context).
Then, through an override of the method DeserializationContext#handleSecondaryContextualization, it will be possible to change the deserialization to make it work.
There is maybe other possibilities but this one is working fine with encryption.
I'm trying to (de)serialize an object that has a property with a type that comes from a maven dependency, so I can't change the class of this type.
The class of this type has a #JsonSerialize and #JsonDeserialize annotation.
However, I want to use the default serializer and deserialzer, because the custom serializer writes an array instead of an object. Is there a way, using annotations, to tell jackson to use the default (de)serializer?
You can disable the annotations using Jackson's mixins feature.
In the following example, any attempt at deserializing to a CustomerObj will result in an exception due to its defective Builder:
#JsonDeserialize(builder = CustomerObj.class)
public class CustomerObj {
public String name;
public int age;
public CustomerObj build() {
throw new RuntimeException("JsonDeserializer invoked");
}
}
Create a mixin with a JsonDeserialize annotation that disables the broken builder:
#JsonDeserialize(builder = java.lang.Void.class)
public static abstract class CustomerMixin { }
Register the mixin on the ObjectMapper instance:
ObjectMapper om = new ObjectMapper();
om.addMixIn(CustomerObj.class, CustomerMixin.class);
Enjoy working deserialization:
final String json = "{\"name\":\"Brian\",\"age\":41}";
CustomerObj customer = om.readValue(json, CustomerObj.class);
I'm working with a 3rd party JSON API, it returns data like this:
{details: {...}, ...}
I use Java Jackson to deserialize this JSON string into a POJO object, the field declaration is :
#JsonProperty("details")
public Details getDetails(){...}
and Details is another class.
Everything is fine until I found that API may return data like this:
{details: false, ...}
If details is empty, it returns false!!! And jackson gave me this exception:
com.fasterxml.jackson.databind.JsonMappingException: Can not instantiate value of type [simple type, class Details] from Boolean value; no single-boolean/Boolean-arg constructor/factory method (through reference chain: ...["details"])
So, how to handle this kind of JSON string? I only need this field to set to null if empty.
The error message from Jackson hints that the library has bulit in support for static factory methods. This is (perhaps) a simpler solution than a custom deserializer:
I created this example POJO, with a static factory method, annotated so that Jackson uses it:
public class Details {
public String name; // example property
#JsonCreator
public static Details factory(Map<String,Object> props) {
if (props.get("details") instanceof Boolean) return null;
Details details = new Details();
Map<String,Object> detailsProps = (Map<String,Object>)props.get("details");
details.name = (String)detailsProps.get("name");
return details;
}
}
test method:
public static void main(String[] args)
{
String fullDetailsJson = "{\"details\": {\"name\":\"My Details\"}} ";
String emptyDetailsJson = "{\"details\": false} ";
ObjectMapper mapper = new ObjectMapper();
try {
Details details = mapper.readValue(fullDetailsJson, Details.class);
System.out.println(details.name);
details = mapper.readValue(emptyDetailsJson, Details.class);
System.out.println(details);
} catch (Exception e) {
e.printStackTrace();
}
}
result is as expected:
My Details
null
Make a custom JsonDeserializer to handle deserializing your Details object in which you either return null if you get false or pass the object to the default deserializer if it's an actual object. Pseudocode:
public class CustomDeserializer extends JsonDeserializer<Details>{
#Override
public Details deserialize(JsonParser jsonParser, DeserializationContext ctx){
//if object use default deserializer else return null
}
}
You'll also have to write an ObjectMapperProvider to register your deserializer like so:
#Provider
public class ObjectMapperProvider implements ContextResolver<ObjectMapper>{
private ObjectMapper mapper;
public ObjectMapperProvider(){
mapper = new ObjectMapper();
SimpleModule sm = new SimpleModule();
sm.addDeserializer(Details.class, new CustomDeserializer());
mapper.registerModule(sm);
}
public ObjectMapper getContext(Class<?> arg0){
return mapper;
}
}
I want to have a Serializer and Deserializer for Class Literals. It should be mapped from / to JSON Strings. And it should also work with generic classes.
I tried writing a custom Deserializer, but it handles all Classes, not just the ones I want:
public class MyDeserializer extends JsonDeserializer<Class<? extends Foo>> {
public Class<? extends Foo> deserialize(JsonParser jp, DeserializationContext ctxt)
throws IOException, JsonProcessingException {
String token = jp.getText();
switch(token) {
case "FOO": return Foo.class;
case "BAR": return Bar.class;
}
return null;
}
}
Jackson will call this Deserializer for EVERY Class<?>. But I just want it to get called for Class<? extends Foo>.
Btw: I want to register this as a global Deserializer. I don't want to use #JsonDeserialize everywhere.
How can I get this working properly?
Thanks in advance.
EDIT: Some more code
POJO:
public class MyPOJO {
public Class<? extends Foo> fooType;
}
JSON:
{
"fooType": "FOO"
}
ObjectMapper usage:
MyPOJO pojo = new MyPOJO();
pojo.fooType = Foo.class;
ObjectMapper om = new ObjectMapper();
String json = om.writeValueAsString(pojo);
What you try to achieve seems to be impossible due to Type Erasure, indeed at runtime your class MyDeserializer is seen as a JsonDeserializer<Class> and your field fooType is simply of type Class not of type Class<? extends Foo> that is actually why your deserializer is called for every Class<?>.
In your case the best solution would be to annotate your fields of type Class<? extends Foo> with #JsonDeserialize(using = MyDeserializer.class) in order to indicate at runtime the deserializer to use but as you don't want to do it, as workaround, assuming that in given class when you have fields of type Class they will all use the same deserializer (custom or default), you could simply modify your class MyDeserializer to check first the current bean class to know whether we should use the custom deserializer or the default one.
You would register it for example as next:
ObjectMapper mapper = new ObjectMapper();
SimpleModule module = new SimpleModule("MyModule")
.addDeserializer(Class.class, new MyDeserializer());
mapper.registerModule(module);
The code of your deserializer would then be:
public class MyDeserializer extends JsonDeserializer<Class<?>> {
// The default deserializer for a property of type Class
private final FromStringDeserializer.Std defaultDeserializer;
public MyDeserializer() {
// Set the default deserializer
this.defaultDeserializer = FromStringDeserializer.findDeserializer(Class.class);
}
public Class<?> deserialize(JsonParser jp, DeserializationContext ctxt)
throws IOException, JsonProcessingException {
// Check whether we should used the custom or default deserializer
// based on the bean class
if (accept(jp.getCurrentValue().getClass())) {
// The custom deserializer
String token = jp.getText();
switch(token) {
case "FOO": return Foo.class;
case "BAR": return Bar.class;
}
return null;
}
// Call the default deserializer
return (Class<?>) defaultDeserializer.deserialize(jp, ctxt);
}
// Returns true if this bean class has fields of type Class that must
// be deserialized with the custom deserializer, false otherwise
public boolean accept(Class<?> beanClass) {
// Implement your logic here
// You could for example have all the bean classes in a Set and
// check that the provided class is in the Set
}
}
My REST service returns following JSON
{
"name": "John",
"id" : 10
}
Can I use Jersey to marshall it into following Bean:
public class User{
private String name;
//getter & setter
}
I wanted to do this with following code but it doesn't work
WebResource webResource = client.resource(url);
webResource.accept(MediaType.APPLICATION_JSON_TYPE);
User user = webResource.get(User.class);
Is this even possible or I have to implement full JSON structure in Java Beans to get it work?
I know that I can parse this JSON with Jackson and any other methods.
With Jackson, easiest way is to configure ObjectMapper like so:
objectMapper.configure(DeserializationConfig.Feature.FAIL_ON_UNKNOWN_PROPERTIES,
false);
Check this sample provider
package com.company.rest.jersey;
#Provider
#Component
#Produces({MediaType.APPLICATION_JSON})
public class JacksonMapperProvider implements ContextResolver<ObjectMapper> {
ObjectMapper mapper;
public JacksonMapperProvider(){
mapper = new ObjectMapper();
mapper.configure(Feature.INDENT_OUTPUT, true);
// Serialize dates using ISO8601 format
// Jackson uses timestamps by default, so use StdDateFormat to get ISO8601
mapper.getSerializationConfig().setDateFormat(new StdDateFormat());
// Deserialize dates using ISO8601 format
// MilliDateFormat simply adds milliseconds to string if missing so it will parse
mapper.getDeserializationConfig().setDateFormat(new MilliDateFormat());
// Prevent exceptions from being thrown for unknown properties
mapper.configure(
DeserializationConfig.Feature.FAIL_ON_UNKNOWN_PROPERTIES,false);
}
#Override
public ObjectMapper getContext(Class<?> aClass) {
return mapper;
}
}
With Jackson :
You have two options:
Jackson works on setters-getters of fields. So, you can just remove getter of field which you want to omit in JSON. ( If you don't need getter at other place.)
Or, you can use the #JsonIgnore annotation of Jackson on getter method of that field and you see there in no such key-value pair in resulted JSON.
#JsonIgnore
public int getSecurityCode(){
return securityCode;
}
In your bean, add the annotation #JsonIgnoreProperties(ignoreUnknown = true) at the class level and it should skip the id property in the JSON since it's not present in the bean.
#JsonIgnoreProperties(ignoreUnknown = true)
public class User{
private String name;
//getter & setter
}
(See http://wiki.fasterxml.com/JacksonAnnotations for details)