Specify jackson writter schema - java

Is it possible to specify a schema for the writing part of Jackson with springboot.
For instance:
I have two pojo classes.
class A {
int a;
B b;
public A() { }
// Getter and Setter
}
class B {
double d;
String s;
public B() { }
// Getter and Setter
}
And I have a service
public SomeClass {
#RequestMapping(path = "/mapping", method = RequestMethod.POST)
public A recupererPESRetourDepuisPlateformeBLES()
return callToSomeMethodThatReturnsA();
}
}
Is there a way to specify that for the return type I want:
the attribute a and b of my object A and the attribute s of the object B.
I want the client to somehow send what we want to the server and the server parses the schema required and returns only it.
I know about #JsonIgnore and #JsonProperty.
I also know GraphQL but I want to stay with Jackson.
Update 1
Example:
I have instantiated in Java such a structure (here represented in JSON to simplify)
"A" {
"a": 12,
"b": {
"d": 23.362,
"s": "Hello world"
}
}
After my request, I want the server to send to the client:
"A" {
"a": 12,
"b": {
"s": "Hello world"
}
}
I don't know what kind of data my client can send to my server to specify the schema of data I want as output. This is part of the question.

I assume that you want to be able to do deep filtering ( be able to filter based on properties of classes A and B). To achieve that you can use following filter:
package com.example.demo.filter;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.JsonStreamContext;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.introspect.Annotated;
import com.fasterxml.jackson.databind.introspect.AnnotationIntrospectorPair;
import com.fasterxml.jackson.databind.introspect.JacksonAnnotationIntrospector;
import com.fasterxml.jackson.databind.ser.PropertyFilter;
import com.fasterxml.jackson.databind.ser.PropertyWriter;
import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter;
import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider;
import com.fasterxml.jackson.databind.type.TypeFactory;
import com.fasterxml.jackson.module.jaxb.JaxbAnnotationIntrospector;
import java.util.Set;
/**
* Sample filtering fields durring json marshalling.
*/
public class JSON {
private static final String DEFAULT_FILTER = "__default";
private static final String DOT = ".";
private static final ObjectMapper MAPPER = new ObjectMapper().setAnnotationIntrospector(
new AnnotationIntrospectorPair(
new FilteringAnnotationInpector(), new JaxbAnnotationIntrospector(TypeFactory.defaultInstance())
)
);
public static String asString(Object object, Set<String> fields) {
PropertyFilter filter = filter(fields);
SimpleFilterProvider provider = new SimpleFilterProvider();
provider.addFilter(DEFAULT_FILTER, filter);
try {
return MAPPER.writer(provider).writeValueAsString(object);
} catch (JsonProcessingException ex) {
throw new RuntimeException("failed to marshall", ex);
}
}
private static PropertyFilter filter(Set<String> fields) {
PropertyFilter filter;
if (fields.size() > 0) {
filter = new DeepFieldFilter(fields);
} else {
filter = SimpleBeanPropertyFilter.serializeAll();
}
return filter;
}
private static class FilteringAnnotationInpector extends JacksonAnnotationIntrospector {
private static final long serialVersionUID = -8722016441050379430L;
#Override
public String findFilterId(Annotated a) {
return DEFAULT_FILTER;
}
}
private static class DeepFieldFilter extends SimpleBeanPropertyFilter {
private final Set<String> includes;
private DeepFieldFilter(Set<String> includes) {
this.includes = includes;
}
private String createPath(PropertyWriter writer, JsonGenerator jgen) {
StringBuilder path = new StringBuilder();
path.append(writer.getName());
JsonStreamContext sc = jgen.getOutputContext();
if (sc != null) {
sc = sc.getParent();
}
while (sc != null) {
if (sc.getCurrentName() != null) {
if (path.length() > 0) {
path.insert(0, DOT);
}
path.insert(0, sc.getCurrentName());
}
sc = sc.getParent();
}
return path.toString();
}
#Override
public void serializeAsField(Object pojo, JsonGenerator gen, SerializerProvider provider, PropertyWriter writer)
throws Exception {
String path = createPath(writer, gen);
if (includes.contains(path)) {
writer.serializeAsField(pojo, gen, provider);
} else {
writer.serializeAsOmittedField(pojo, gen, provider);
}
}
}
}
Here is complete example how to use this.
So, for example, if you want to return only property "a" of object A, first you will create controller method like this:
#GetMapping("/test")
public String test(#RequestBody Set<String> filterFields) {
B b = new B();
b.setD(23);
b.setS("b test");
A a = new A();
a.setA(1);
a.setB(b);
return JSON.asString(a, filterFields);
}
And if you send request like this:
The output should be {"a":1}
If you want to display property b of A class, with field s, you will send request like this:
And the output shoud be {"b":{"s":"b test"}}
As far as I can see, there is no other way to achieve what you want (using just Jackson).

Spring boot dynamic filtering concept should help in this case. Modify the return type of the method recupererPESRetourDepuisPlateformeBLES() to MappingJacksonValue . Here are the code changes needed to be made:
import com.fasterxml.jackson.databind.ser.FilterProvider;
import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter;
import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider;
public SomeClass {
#RequestMapping(path = "/mapping", method = RequestMethod.POST)
public MappingJacksonValue recupererPESRetourDepuisPlateformeBLES()
SimpleBeanPropertyFilter propertyFilter = SimpleBeanPropertyFilter.filterOutAllExcept("s");
FilterProvider filterProvider = new SimpleFilterProvider().addFilter("dynamicFilter", propertyFilter);
B b = new B();
b.setD(1d);
b.setS("someString");
A a = new A();
a.setA(1);
a.setB(b);
MappingJacksonValue value = new MappingJacksonValue(a);
value.setFilters(filterProvider);
return value;
}
}
Annotate the target Response class with #JsonFilter like below
import com.fasterxml.jackson.annotation.JsonFilter;
#JsonFilter("dynamicFilter")
public class B {
double d;
String s;
public B() {
}
}
Hope. This should solve the problem.

Related

Adding a custom TypeAdapterFactory for Google GSON to detect use of annotations to return custom TypeAdapter dynamically

Let us start with sharing working and pastable code (requires google gson package):
package mypackage;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.TypeAdapter;
import com.google.gson.TypeAdapterFactory;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
import java.io.*;
import java.lang.annotation.Annotation;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
public final class ALL {
static final Gson GSON = new GsonBuilder().registerTypeAdapterFactory(new Factory()).create();
/////////////////////////////////////////////////////////////////////
#Target( { METHOD, FIELD, ANNOTATION_TYPE, TYPE })
#Retention(RUNTIME)
public #interface Serialize {}
/////////////////////////////////////////////////////////////////////
public static void main(String[] args) {
Test test = new Test();
String json = GSON.toJson(test);
System.out.println(json);
}
/////////////////////////////////////////////////////////////////////
public static final class Test {
#Serialize
String abc = "def";
}
/////////////////////////////////////////////////////////////////////
public static final class Factory implements TypeAdapterFactory {
#Override
public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
Serialize annotation = type.getRawType().getAnnotation(Serialize.class);
boolean annotationPresent = type.getRawType().isAnnotationPresent(Serialize.class);
Annotation[] annotations = type.getRawType().getAnnotations();
if (annotationPresent) {
System.out.println("11111111111111");
}
if (annotation != null) {
return new Adapter<>();
}
return gson.getDelegateAdapter(this, type);
}
}
/////////////////////////////////////////////////////////////////////
public static final class Adapter<T> extends TypeAdapter<T> {
private static final java.util.Base64.Encoder ENCODER = java.util.Base64.getEncoder();
private static final java.util.Base64.Decoder DECODER = java.util.Base64.getDecoder();
#Override
public T read(JsonReader in) throws IOException {
in.beginObject();
String a = in.nextString();
in.endObject();
try {
return deserialize( DECODER.decode(a) );
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
#Override
public void write(JsonWriter out, T value) throws IOException {
out.value( encode(serialize(value)) );
}
private String encode(byte[] serialize) {
return ENCODER.encodeToString( serialize );
}
private byte[] serialize(T value) throws IOException {
try (ByteArrayOutputStream out = new ByteArrayOutputStream(); ObjectOutputStream os = new ObjectOutputStream(out); ) {
os.writeObject(value);
return out.toByteArray();
}
}
private T deserialize(byte[] bytes) throws IOException, ClassNotFoundException {
try (ByteArrayInputStream in = new ByteArrayInputStream(bytes); ObjectInputStream is = new ObjectInputStream(in); ) {
return (T) is.readObject();
}
}
}
}
If we look at Test class, the goal is to output something else if the annotation #Serialize is present. In this case we output bytes in String. And when we then read this back, we would like to deserialize it.
Other ways of understanding the goal is to think of maybe using an annotation you would like to encrypt a value and you could decrypt it on readback.
This should be possible, no?
I know i can register TypeAdapters based on field type, however, I would like to be able to use annotations to declare intent instead.
No wrapper classes. You can create a custom JsonSerializer but this requires registering.
In the example above, the type.getRawType().getAnnotation(Serialize.class); is always returning null and Annotation[] annotations = type.getRawType().getAnnotations() always empty, so unable to detect using the factory.
Unsure how to detect the annotation dynamically.
Do you know?
How about using #JsonAdapter? You anyway need to know how to do de-/crypting and need to implement tha per type. For string in your case, for example:
public class CryptoAdapter extends TypeAdapter<String> {
#Override
public void write(JsonWriter out, String value) throws IOException {
out.jsonValue(org.apache.commons.lang3.StringUtils.reverse(value));
}
#Override
public String read(JsonReader in) throws IOException {
return org.apache.commons.lang3.StringUtils.reverse(in.nextString());
}
}
Usage:
public class Test {
#JsonAdapter(CryptoAdapter.class)
String abc = "def";
}
The problem is that Gson does not provide (to my knowledge) any direct means to create some own field processor that lets user to read the field/class member annotations.
In other words you need an access to the field during de-/serialization and that seem not to be possible in an easy way.
That is why there is this #JsonAdapter.
If interested to study more clone source code from GitHub and check:
public final class ReflectiveTypeAdapterFactory implements TypeAdapterFactory
Unfortunately final. There is a method named createBoundField (which I think is the logic behind recognizing #JsonAdapter for fields) and the path and overriding that logic is not so straightforward.
For classes there seems to be solution quite similar to yours:
public final class JsonAdapterAnnotationTypeAdapterFactory
implements TypeAdapterFactory
Both above mentioned are added to the list of TypeAdapterFactories when a new Gson is created.

Jackson: Serializing object with list of tags [duplicate]

Let's say I have a bean:
public class Msg {
private int code;
private Object data;
... Getter/setters...
}
And I convert it into JSON or XML with this kind of test code:
public String convert() {
Msg msg = new Msg();
msg.setCode( 42 );
msg.setData( "Are you suggesting coconuts migrate?" );
ObjectMapper mapper = new ObjectMapper();
return mapper.writeValueAsString( msg );
}
The output will be somehow like that :
{"code":42,"data":"Are you suggesting coconuts migrate?"}
Now let's say I want to replace the 'data' attribute with some dynamic name:
public String convert(String name) {
Msg msg = new Msg();
msg.setCode( 42 );
msg.setData( "Are you suggesting coconuts migrate?" );
ObjectMapper mapper = new ObjectMapper();
// ...DO SOMETHING WITH MAPPER ...
return mapper.writeValueAsString( msg );
}
If I call the function convert( "toto") I woukld like to have this output:
{"code":42,"toto":"Are you suggesting coconuts migrate?"}
If I call the function convert( "groovy") I woukld like to have this output:
{"code":42,"groovy":"Are you suggesting coconuts migrate?"}
Of course I could do a String replace after JSON creation, but if you have an answer with a programmatic approach I'll take it.
Thanks
You can use PropertyNamingStrategy class to override class property. See simple implementation of this class:
class ReplaceNamingStrategy extends PropertyNamingStrategy {
private static final long serialVersionUID = 1L;
private Map<String, String> replaceMap;
public ReplaceNamingStrategy(Map<String, String> replaceMap) {
this.replaceMap = replaceMap;
}
#Override
public String nameForGetterMethod(MapperConfig<?> config, AnnotatedMethod method, String defaultName) {
if (replaceMap.containsKey(defaultName)) {
return replaceMap.get(defaultName);
}
return super.nameForGetterMethod(config, method, defaultName);
}
}
Example program could look like this:
import java.io.IOException;
import java.util.Collections;
import java.util.Map;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
import com.fasterxml.jackson.databind.cfg.MapperConfig;
import com.fasterxml.jackson.databind.introspect.AnnotatedMethod;
public class JacksonProgram {
public static void main(String[] args) throws IOException {
Msg msg = new Msg();
msg.setCode(42);
msg.setData("Are you suggesting coconuts migrate?");
System.out.println(convert(msg, "test"));
System.out.println(convert(msg, "toto"));
System.out.println(convert(msg, "groovy"));
}
public static String convert(Msg msg, String name) throws IOException {
ObjectMapper mapper = new ObjectMapper();
mapper.setPropertyNamingStrategy(new ReplaceNamingStrategy(Collections.singletonMap("data", name)));
return mapper.writeValueAsString(msg);
}
}
Above program prints:
{"code":42,"test":"Are you suggesting coconuts migrate?"}
{"code":42,"toto":"Are you suggesting coconuts migrate?"}
{"code":42,"groovy":"Are you suggesting coconuts migrate?"}
One possibility would be to use so-called "any getter":
public class Msg {
public int code;
#JsonAnyGetter
public Map<String,Object> otherFields() {
Map<String,Object> extra = new HashMap<String,Object>();
extra.put("data", findDataObject()); // or whatever mechanism you want
extra.put("name", "Some Name");
return extra;
}
}
so that you can return arbitrary set of dynamic properties.
There is also matching "any getter" (#JsonAnyGetter) mechanism you can use to accept additional properties.

Why opencsv capitalizing csv headers while writing to file

While writing Beans to CSV file by using OpenCSV 4.6, all the headers are changing to uppercase. Eventhough bean has #CsvBindByName annotation it is changing to uppercase.
Java Bean:
public class ProjectInfo implements Serializable {
#CsvBindByName(column = "ProjectName",required = true)
private String projectName;
#CsvBindByName(column = "ProjectCode",required = true)
private String projectCode;
#CsvBindByName(column = "Visibility",required = true)
private String visibility;
//setters and getters
}
Main method
public static void main(String[] args) throws IOException {
Collection<Serializable> projectInfos = getProjectsInfo();
try(BufferedWriter writer = new BufferedWriter(new FileWriter("test.csv"))){
StatefulBeanToCsvBuilder builder = new StatefulBeanToCsvBuilder(writer);
StatefulBeanToCsv beanWriter = builder
.withSeparator(';')
.build();
try {
beanWriter.write(projectInfos.iterator());
writer.flush();
} catch (CsvDataTypeMismatchException | CsvRequiredFieldEmptyException e) {
throw new RuntimeException("Failed to download admin file");
}
}
}
Expected Result:
"ProjectCode";"ProjectName";"Visibility"
"ANY";"Country DU";"1"
"STD";"Standard";"1"
"TST";"Test";"1"
"CMM";"CMMTest";"1"
Acutal Result:
"PROJECTCODE";"PROJECTNAME";"VISIBILITY"
"ANY";"Country DU";"1"
"STD";"Standard";"1"
"TST";"Test";"1"
"CMM";"CMMTest";"1"
I don't have option to use ColumnMappingStrategy because I have to build this method as a generic solution.
can anyone suggest me how to write the headers as it is?
It happens, because the code in HeaderColumnNameMappingStrategy uses toUpperCase() for storing and retrieving the field names.
You could use the HeaderColumnNameTranslateMappingStrategy instead and create the mapping by reflection.
public class AnnotationStrategy extends HeaderColumnNameTranslateMappingStrategy
{
public AnnotationStrategy(Class<?> clazz)
{
Map<String,String> map=new HashMap<>();
//To prevent the column sorting
List<String> originalFieldOrder=new ArrayList<>();
for(Field field:clazz.getDeclaredFields())
{
CsvBindByName annotation = field.getAnnotation(CsvBindByName.class);
if(annotation!=null)
{
map.put(annotation.column(),annotation.column());
originalFieldOrder.add(annotation.column());
}
}
setType(clazz);
setColumnMapping(map);
//Order the columns as they were created
setColumnOrderOnWrite((a,b) -> Integer.compare(originalFieldOrder.indexOf(a), originalFieldOrder.indexOf(b)));
}
#Override
public String[] generateHeader(Object bean) throws CsvRequiredFieldEmptyException
{
String[] result=super.generateHeader(bean);
for(int i=0;i<result.length;i++)
{
result[i]=getColumnName(i);
}
return result;
}
}
And, assuming that there is only one class of items (and always at least one item), the creation of beanWriter has to be expanded:
StatefulBeanToCsv beanWriter = builder.withSeparator(';')
.withMappingStrategy(new AnnotationStrategy(projectInfos.iterator().next().getClass()))
.build();
Actually, HeaderColumnNameMappingStrategy uses toUpperCase() for storing and retrieving the field names.
In order to use custom field name you have to annotate you field with #CsvBindByName
#CsvBindByName(column = "Partner Code" )
private String partnerCode;
By default it will be capitalized to PARTNER CODE because of the above reason.
so, in order to take control over it we have to write a class implementing HeaderColumnNameTranslateMappingStrategy. With csv 5.0 and java8 i have implemented like this
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
import com.opencsv.bean.CsvBindByName;
import com.opencsv.bean.HeaderColumnNameTranslateMappingStrategy;
import com.opencsv.exceptions.CsvRequiredFieldEmptyException;
public class AnnotationStrategy<T> extends HeaderColumnNameTranslateMappingStrategy<T> {
Map<String, String> columnMap = new HashMap<>();
public AnnotationStrategy(Class<? extends T> clazz) {
for (Field field : clazz.getDeclaredFields()) {
CsvBindByName annotation = field.getAnnotation(CsvBindByName.class);
if (annotation != null) {
columnMap.put(field.getName().toUpperCase(), annotation.column());
}
}
setType(clazz);
}
#Override
public String getColumnName(int col) {
String name = headerIndex.getByPosition(col);
return name;
}
public String getColumnName1(int col) {
String name = headerIndex.getByPosition(col);
if(name != null) {
name = columnMap.get(name);
}
return name;
}
#Override
public String[] generateHeader(T bean) throws CsvRequiredFieldEmptyException {
String[] result = super.generateHeader(bean);
for (int i = 0; i < result.length; i++) {
result[i] = getColumnName1(i);
}
return result;
}
}
I have tried other solutions but it doesn't work when the property name and column name are not the same.
I am using 5.6. My solution is to reuse the strategy.
public class CsvRow {
#CsvBindByName(column = "id")
private String id;
// Property name and column name are different
#CsvBindByName(column = "country_code")
private String countryCode;
}
// We are going to reuse this strategy
HeaderColumnNameMappingStrategy<CsvRow> strategy = new HeaderColumnNameMappingStrategy<>();
strategy.setType(CsvRow.class);
// Build the header line which respects the declaration order
// So its value will be "id,country_code"
String headerLine = Arrays.stream(CsvRow.class.getDeclaredFields())
.map(field -> field.getAnnotation(CsvBindByName.class))
.filter(Objects::nonNull)
.map(CsvBindByName::column)
.collect(Collectors.joining(","));
// Let the library to initialize column details in the strategy
try (StringReader stringReader = new StringReader(headerLine);
CSVReader reader = new CSVReader(stringReader)) {
CsvToBean<CsvRow> csv = new CsvToBeanBuilder<CsvRow>(reader)
.withType(CsvRow.class)
.withMappingStrategy(strategy)
.build();
for (CsvRow csvRow : csv) {}
}
The strategy is ready for writing csv file.
try (OutputStream outputStream = Files.newOutputStream(Path.of("test.csv"));
OutputStreamWriter writer = new OutputStreamWriter(outputStream)) {
StatefulBeanToCsv<CsvRow> csv = new StatefulBeanToCsvBuilder<CsvRow>(writer)
.withMappingStrategy(strategy)
.withThrowExceptions(true)
.build();
csv.write(csvRows);
}
Using opencsv 5.0 and Java 8, I had to modify AnnotationStrategy class code as follows to had it compiled :
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
import com.opencsv.bean.CsvBindByName;
import com.opencsv.bean.HeaderColumnNameTranslateMappingStrategy;
import com.opencsv.exceptions.CsvRequiredFieldEmptyException;
public class AnnotationStrategy<T> extends HeaderColumnNameTranslateMappingStrategy<T> {
public AnnotationStrategy(Class<? extends T> clazz) {
Map<String, String> map = new HashMap<>();
for (Field field : clazz.getDeclaredFields()) {
CsvBindByName annotation = field.getAnnotation(CsvBindByName.class);
if (annotation != null) {
map.put(annotation.column(), annotation.column());
}
}
setType(clazz);
setColumnMapping(map);
}
#Override
public String[] generateHeader(T bean) throws CsvRequiredFieldEmptyException {
String[] result = super.generateHeader(bean);
for (int i = 0; i < result.length; i++) {
result[i] = getColumnName(i);
}
return result;
}
}

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}}

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.

Categories

Resources