I have a java project where one of my packages contain a lot of dataclasses. These classes are used with deserialization. Currently the package depends on fasterXML only because one of my classes has this field:
String eMail;
I would like to remove the dependency to fasterXML but I have the following problem.
val mapper = new ObjectMapper();
// Produces UnrecognizedPropertyException: Unrecognized field "eMail"
// because by default it expects eMail with a lower case 'm'
val res1 = mapper.readValue("{\"eMail\":\"asd\"}", PP.class);
// BTW this Produces '{"email":"asd"}' --> 'm' in eMail is lower case!
val res2 = mapper.writeValueAsString(new PP() {{setEMail("asd");}});
Where the PP class is
#Getter
#Setter
private static class PP {
private String eMail;
}
I CANNOT change the json format!
Is it possible to somehow correctly readValue(PP) without using the JsonProperty annotation? Maybe configuring the objectMapper somehow?
The only field I have problem is this one. :(
Thanks!
You could make an intermediate class
#Getter
#Setter
private static class IntermediatePP {
private String email;
public PP convert() {
PP output = new PP();
output.setEMail(this.email);
return output
}
}
Then change your code to
val res1 = mapper.readValue("{\"eMail\":\"asd\"}",
IntermediatePP.class).convert();
Related
As an example class:
#Getter #Setter
public static class SomeClass {
private String notNull;
private String nullSetEmpty;
private String notExists;
}
Deserialization of null values to empty is possible by overriding configuration, like:
String json = " {\"notNull\": \"a value\", \"nullSetEmpty\": null}";
ObjectMapper om = new ObjectMapper();
om.configOverride(String.class)
.setSetterInfo(JsonSetter.Value.forValueNulls(Nulls.AS_EMPTY));
SomeClass sc = om.readValue(json, SomeClass.class);
System.out.print(om.writerWithDefaultPrettyPrinter().writeValueAsString(sc));
This produces:
{
"notNull" : "a value",
"nullSetEmpty" : "",
"notExists" : null
}
But how about this notExists. It is possible to add default value to each class having the problem but is there any generic way to do that like configOverride does so that Jackson handles that?
you can just define default value in your data class
#Getter
#Setter
public static class SomeClass {
private String notNull;
private String nullSetEmpty;
private String notExists = "default-value";
}
I am trying to deserialize JSON into a custom POJO that I am not able to modify. That POJO has annotations from a different custom internal serialization framework that I'm not able to use. How can I create a custom deserializer that will respect these annotations?
Here is an example POJO:
public class ExampleClass {
#Property(name = "id")
public String id;
#Property(name = "time_windows")
#NotNull
public List<TimeWindow> timeWindows = new ArrayList<>();
public static class TimeWindow {
#Property(name = "start")
public Long start;
#Property(name = "end")
public Long end;
}
}
So in this case, the deserializer would look for fields in the JSON that correspond to the Property annotations, and use the value in that annotation to decide what field to grab. If a property doesn't have the Property annotation, it should be ignored.
I have been going through the Jackson docs but haven't been able to find exactly what I need. Is this a place where an AnnotationIntrospector would be useful? Or possibly a ContextualDeserializer?
Any pointers in the right direction would be greatly appreciated!
Update: I tried implementing the advice in the comments, but without success.
Here is my initial implementation of the introspector:
class CustomAnnotationInspector : JacksonAnnotationIntrospector () {
override fun hasIgnoreMarker(m: AnnotatedMember?): Boolean {
val property = m?.getAnnotation(Property::class.java)
return property == null
}
override fun findNameForDeserialization(a: Annotated?): PropertyName {
val property = a?.getAnnotation(Property::class.java)
return if (property == null) {
super.findNameForDeserialization(a)
} else {
PropertyName(property.name)
}
}
}
And here is where I actually use it:
// Create an empty instance of the request object.
val paramInstance = nonPathParams?.type?.getDeclaredConstructor()?.newInstance()
// Create new object mapper that will write values from
// JSON into the empty object.
val mapper = ObjectMapper()
// Tells the mapper to respect custom annotations.
mapper.setAnnotationIntrospector(CustomAnnotationInspector())
// Write the contents of the request body into the StringWriter
// (this is required for the mapper.writeValue method
val sw = StringWriter()
sw.write(context.bodyAsString)
// Deserialize the contents of the StringWriter
// into the empty POJO.
mapper.writeValue(sw, paramInstance)
Unfortunately it seems that findNameForDeserialization is never called, and none of the JSON values are written into paramInstance. Can anybody spot where I'm going wrong?
Thank you!
Update 2: I changed the code slightly, I'm now able to identify the property names but Jackson is failing to create an instance of the object.
Here's my new code:
val mapper = ObjectMapper()
// Tells the mapper to respect CoreNg annotations.
val introspector = CustomAnnotationInspector()
mapper.setAnnotationIntrospector(introspector)
val paramInstance = mapper.readValue(context.bodyAsString,nonPathParams?.type)
My breakpoints in the custom annotation introspector are getting hit. But I'm getting the following exception:
com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `app.employee.api.employee.BOUpsertEmployeeRequest` (no Creators, like default constructor, exist): cannot deserialize from Object value (no delegate- or property-based Creator)
Here is the POJO I'm trying to deserialize:
public class BOUpsertEmployeeRequest {
public BOUpsertEmployeeRequest () { }
#NotNull
#Property(name = "xref_code")
public String xrefCode;
#Property(name = "first_name")
public String firstName;
#Property(name = "last_name")
public String lastName;
#Property(name = "email_address")
public String emailAddress;
#Property(name = "phone")
public String phone;
#Property(name = "address")
public List<String> address;
#Property(name = "employment_status")
public String employmentStatus;
#Property(name = "pay_type")
public String payType;
#Property(name = "position")
public String position;
#Property(name = "skills")
public List<String> skills;
#Property(name = "gender")
public String gender;
}
As far as I can tell it has a default constructor. Anybody have any idea what the problem is?
Thank you!
Method hasIgnoreMarker is called not only for fields, but also for the constructor, including the virtual one:
Method called to check whether given property is marked to be ignored. This is used to determine whether to ignore properties, on per-property basis, usually combining annotations from multiple accessors (getters, setters, fields, constructor parameters).
In this case you should ignore only fields, that are not marked properly:
static class CustomAnnotationIntrospector extends JacksonAnnotationIntrospector {
#Override
public PropertyName findNameForDeserialization(Annotated a) {
Property property = a.getAnnotation(Property.class);
if (property == null) {
return PropertyName.USE_DEFAULT;
} else {
return PropertyName.construct(property.name());
}
}
#Override
public boolean hasIgnoreMarker(AnnotatedMember m) {
return m instanceof AnnotatedField
&& m.getAnnotation(Property.class) == null;
}
}
Example:
class Pojo {
// #Property(name = "id")
Integer id;
// #Property(name = "number")
Integer number;
#Property(name = "assure")
Boolean assure;
#Property(name = "person")
Map<String, String> person;
}
String json =
"{\"id\" : 1, \"number\" : 12345, \"assure\" : true," +
" \"person\" : {\"name\" : \"John\", \"age\" : 23}}";
ObjectMapper mapper = new ObjectMapper();
mapper.setAnnotationIntrospector(new CustomAnnotationIntrospector());
Pojo pojo = mapper.readValue(json, Pojo.class);
System.out.println(pojo);
Pojo{id=null, number=null, assure=true, person={name=John, age=23}}
Note: Custom Property annotation should have RetentionPolicy.RUNTIME (same as JsonProperty annotation):
#Retention(RetentionPolicy.RUNTIME)
public #interface Property {
String name();
}
I will suggest a different approach:
In the runtime, with the bytecode instrumentation library Byte Buddy and its Java agent, re-annotate the fields with the proper Jackson Annotations. Simply implement the logic via reflection. See the following example:
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Field;
import net.bytebuddy.ByteBuddy;
import net.bytebuddy.agent.ByteBuddyAgent;
import net.bytebuddy.description.annotation.AnnotationDescription;
import net.bytebuddy.dynamic.DynamicType.Builder;
import net.bytebuddy.dynamic.DynamicType.Builder.FieldDefinition.Valuable;
import net.bytebuddy.dynamic.loading.ClassReloadingStrategy;
import net.bytebuddy.matcher.ElementMatchers;
#Target(ElementType.FIELD)
#Retention(RetentionPolicy.RUNTIME)
#interface MyJsonIgnore {
}
#Target(ElementType.FIELD)
#Retention(RetentionPolicy.RUNTIME)
#interface MyJsonProperty {
String name();
}
public class Sample {
public static void main(String[] args) throws JsonProcessingException {
ByteBuddyAgent.install();
ClassReloadingStrategy classReloadingStrategy = ClassReloadingStrategy.fromInstalledAgent();
ByteBuddy byteBuddy = new ByteBuddy();
AnnotationDescription jsonIgnoreDesc =
AnnotationDescription.Builder.ofType(JsonIgnore.class).build();
Builder<Person> personBuilder = byteBuddy.redefine(Person.class);
for (Field declaredField : Person.class.getDeclaredFields()) {
Valuable<Person> field = personBuilder.field(ElementMatchers.named(declaredField.getName()));
MyJsonProperty myJsonProperty = declaredField.getAnnotation(MyJsonProperty.class);
if (myJsonProperty != null) {
AnnotationDescription jsonPropertyDesc =
AnnotationDescription.Builder.ofType(JsonProperty.class)
.define("value", myJsonProperty.name())
.build();
personBuilder = field.annotateField(jsonPropertyDesc);
}
MyJsonIgnore myJsonIgnore = declaredField.getAnnotation(MyJsonIgnore.class);
if (myJsonIgnore != null) {
personBuilder = field.annotateField(jsonIgnoreDesc);
}
}
personBuilder.make().load(Sample.class.getClassLoader(), classReloadingStrategy);
Person person = new Person("Utku", "Ozdemir", "Berlin");
ObjectMapper objectMapper = new ObjectMapper();
String jsonString = objectMapper.writeValueAsString(person);
System.out.println(jsonString);
}
}
class Person {
#MyJsonProperty(name = "FIRST")
private String firstName;
#MyJsonProperty(name = "LAST")
private String lastName;
#MyJsonIgnore private String city;
public Person(String firstName, String lastName, String city) {
this.firstName = firstName;
this.lastName = lastName;
this.city = city;
}
}
In the example above, I
create MyJsonProperty and MyJsonIgnore annotations and a Person class for the demonstration purpose
instrument the current Java process with the Byte buddy agent
create a bytebuddy builder to redefine the Person class
loop over the fields of the Person class and check for these annotations
add an additional annotation to each of those fields (on the builder), Jackson's JsonProperty (with the correct field name mapping) and JsonIgnore.
after being done with the fields, make the new class bytecode and load it to the current classloader using the byte buddy agent's class reloading mechanism
write a person object to the stdout.
It prints, as expected:
{"FIRST":"Utku","LAST":"Ozdemir"}
(the field city is ignored)
This solution might feel like an overkill, but on the other side, it is pretty generic solution - with a few changes in the logic, you could handle all the 3rd party classes (which you are not able to modify) instead of handling them case by case.
This question already has answers here:
Case insensitive JSON to POJO mapping without changing the POJO
(8 answers)
Closed 3 years ago.
I have a JSON value as follows in String format.
{
"Sample": {
"name": "some name",
"key": "some key"
},
"Offering": {
"offer": "some offer",
"amount": 100
}
}
Now if I try to map this as follows, it works and maps fine.
//mapper is ObjectMapper;
//data is the above json in String format
Map vo = mapper.readValue(data, Map.class);
But I want to map it to a custom Data class as follows.
Data vo = mapper.readValue(data, Data.class);
When I do this, the result of vo is null.
Refer to following on how the Data class is structured.
#Getter
#Setter
public class Data {
private Sample sample;
private Offering offering;
}
#Getter
#Setter
public class Offering {
public String offer;
public int amount;
}
#Getter
#Setter
public class Sample {
private String name;
private String key;
}
Please advice what I am doing wrong. Thanks.
There seems to be issue with Word Case here.
Its "Sample" in your json.
But its "sample" in java file.
Similarly for Offering.
You can of-course use #JsonProperty if you want to map without changing the case.
There are two options :
if you can change your json - you have to change Sample to sample and Offering to offering
Change your Data class to :
#Getter
#Setter
public class Data {
#JsonProperty("Sample")
private Sample sample;
#JsonProperty("Offering")
private Offering offering;
}
In the second option you have to tell Jackson what properties of your input json you want to map to which properties of your class, because by default it will try to map to lowercase properties names.
It may be due to different mapping names of fields in json string and Demo model. "Sample" in json string but "sample" in model class.
You can use #JsonProperty
#JsonProperty("Sample")
private Sample sample;
#JsonProperty("Offering")
private Offering offering;
There is an abstract class Product and another class SomeProduct which extends Product.
Product:
#JsonTypeInfo
(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.PROPERTY,
property = "type"
)
#JsonSubTypes
({
#JsonSubTypes.Type(value = SomeProduct.class, name = Product.PRODUCT_TYPE_SOME)
})
public abstract class Product
{
static final String PRODUCT_TYPE_SOME = "some_product";
}
SomeProduct:
public class SomeProduct extends Product
{
#JsonProperty("status")
#JsonSerialize(include = JsonSerialize.Inclusion.NON_DEFAULT)
private int status;
public int getStatus()
{
return status;
}
public void setStatus(int status)
{
this.status = status;
}
}
There will be more classes (different products) which will extend Product.
When I serialize it using ObjectMapper,
ObjectMapper mapper = new ObjectMapper();
Product p = new SomeProduct();
String json = mapper.writeValueAsString(p);
this is the output:
{"type":"some_product"}
Now, when I try to deserialize it back,
Product x = mapper.convertValue(json, Product.class);
this exception is thrown:
java.lang.IllegalArgumentException: Unexpected token (VALUE_STRING), expected FIELD_NAME: missing property 'type' that is to contain type id (for class com.shubham.model.Product)
at [Source: N/A; line: -1, column: -1]
What am I doing wrong here ? I've looked on SO and found a question where defaultImpl was used in JsonTypeInfo. But I can't deserialize the json back to a "Default Impl" since the JSON will always be valid for a specific implementation.
Using Jackson 2.4.3
You are wrongly using mapper.convertValue(json, Product.class);. You should use:
mapper.readValue(json, Product.class);
convertValue use:
Convenience method for doing two-step conversion from given value, into
instance of given value type, if (but only if!) conversion is needed.
If given value is already of requested type, value is returned as is.
I've been trying to upgrade the JSON modules to use the FasterXML (2.6.3) versions of Jackson instead of the old Codehaus modules. During the upgrade, I've noticed that the naming strategy differs when using FasterXML instead of Codehaus.
Codehaus was more flexible when it came to the naming strategy. The test below highlights the issue I'm facing with FasterXML. How can I configure the ObjectMapper so it follows the same strategy like Codehaus?
I cannot alter the JSONProperty annotations as there are hundreds of them. I would like the upgrade to be backwards compatible with respect to the naming strategy.
import java.io.IOException;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
/*import org.codehaus.jackson.annotate.JsonIgnoreProperties;
import org.codehaus.jackson.annotate.JsonProperty;
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.map.PropertyNamingStrategy;*/
import org.junit.Assert;
import org.junit.Test;
public class JSONTest extends Assert {
#JsonIgnoreProperties(ignoreUnknown = true)
public static class Product {
#JsonProperty(value = "variationId")
private String variantId;
#JsonProperty(value = "price_text")
private String priceText;
#JsonProperty(value = "listPrice")
public String listPrice;
#JsonProperty(value = "PRODUCT_NAME")
public String name;
#JsonProperty(value = "Product_Desc")
public String description;
}
private static final String VALID_PRODUCT_JSON =
"{ \"list_price\": 289," +
" \"price_text\": \"269.00\"," +
" \"variation_id\": \"EUR\"," +
" \"product_name\": \"Product\"," +
" \"product_desc\": \"Test\"" +
"}";
#Test
public void testDeserialization() throws IOException {
ObjectMapper mapper = new ObjectMapper();
mapper.setPropertyNamingStrategy(PropertyNamingStrategy.CAMEL_CASE_TO_LOWER_CASE_WITH_UNDERSCORES);
Product product = mapper.readValue(VALID_PRODUCT_JSON, Product.class);
System.out.println(mapper.writerWithDefaultPrettyPrinter().writeValueAsString(product));
assertNotNull(product.listPrice);
assertNotNull(product.variantId);
assertNotNull(product.priceText);
assertNotNull(product.name);
assertNotNull(product.description);
}
}
#JsonProperty overrides any PropertyNamingStrategy in fasterxml since version 2.4.0. However, yet-to-be-released version 2.7.0 will provide a feature to allow you to opt back in to the old behavior. There is also an unimplemented suggestion to toggle this at the per-annotation level, but that would not really help you.
It appears that Codehaus does apply the PropertyNamingStrategy on top of the #JsonProperty values when mapping, although I can't find any clear docs on that. This appears to have been the behavior in fasterxml before 2.4.0 as well. Here is another example of someone noticing the same difference in behavior.
Although the solution provided by SkinnyJ is perfect for your problem, but if you can't wait till 2.7 is released, you can apply the below hack to get around the problem.
The idea is to transform the incoming JSON to match the attributes in your bean definition. Below code does that. Following points should be noted:
If you are dealing with nested structures, you will have to implement a recursive function to achieve this transformation.
There is a little overhead involved in doing the transformation.
Code:
public class JSONTest extends Assert {
#JsonIgnoreProperties(ignoreUnknown = true)
public static class Product {
#JsonProperty(value = "variationId")
private String variantId;
#JsonProperty(value = "price_text")
private String priceText;
#JsonProperty(value = "listPrice")
public String listPrice;
#JsonProperty(value = "PRODUCT_NAME")
public String name;
#JsonProperty(value = "Product_Desc")
public String description;
}
private static final String VALID_PRODUCT_JSON =
"{ \"list_price\": 289," +
" \"price_text\": \"269.00\"," +
" \"variation_id\": \"EUR\"," +
" \"product_name\": \"Product\"," +
" \"product_desc\": \"Test\"" +
"}";
#Test
public void testDeserialization() throws IOException {
ObjectMapper mapper = new ObjectMapper();
mapper.setPropertyNamingStrategy(PropertyNamingStrategy.CAMEL_CASE_TO_LOWER_CASE_WITH_UNDERSCORES);
//Capture the original JSON in org.json.JSONObject
JSONObject obj = new JSONObject(VALID_PRODUCT_JSON);
JSONArray keys = obj.names();
//New json object to be created using property names defined in bean
JSONObject matchingJson = new JSONObject();
//Map of lowercased key to original keys in incoming json. eg: Prod_id > prodid
Map<String, String> jsonMappings = new LinkedHashMap<String, String>();
for (int i = 0; i < keys.length(); i++) {
String key = lowerCaseWithoutUnderScore(keys.getString(i));
String value = keys.getString(i);
jsonMappings.put(key, value);
}
/*
* Iternate all jsonproperty beans and create new json
* such that keys in json map to that defined in bean
*/
Field[] fields = Product.class.getDeclaredFields();
for (Field field : fields) {
JsonProperty prop = field.getAnnotation(JsonProperty.class);
String propNameInBean = prop.value();
String keyToLook = lowerCaseWithoutUnderScore(propNameInBean);
String keyInJson = jsonMappings.get(keyToLook);
matchingJson.put(propNameInBean, obj.get(keyInJson));
}
String json = matchingJson.toString();
System.out.println(json);
//Pass the matching json to Object mapper
Product product = mapper.readValue(json, Product.class);
System.out.println(mapper.writerWithDefaultPrettyPrinter().writeValueAsString(product));
assertNotNull(product.listPrice);
assertNotNull(product.variantId);
assertNotNull(product.priceText);
assertNotNull(product.name);
assertNotNull(product.description);
}
private String lowerCaseWithoutUnderScore(String key){
return key.replaceAll("_", "").toLowerCase();
}
}