I have the following XML structure:
<participants>
<participant side="AWAY">
<team id="18591" name="Orlando Apollos" />
</participant>
<participant side="HOME">
<team id="18594" name="Memphis Express" />
</participant>
</participants>
If I am using the FasterXML Jackson library with JAXB annotations how can I bind the participants fields to two different Participant objects participantHome and participantAway using the side property of AWAY and HOME to bind the fields.
Using the following object won't work obviously because there are duplicate fields:
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
#XmlRootElement(name = "participants")
public class Participants {
#XmlElement(name = "participant")
Participant participantHome;
#XmlElement(name = "participant")
Participant participantAway;
}
How can I dynamically bind those elements using JAXB annotations or a custom JAXB implementation?
You need to write custom deserialiser because there is no annotation which allow to bind list item to given property in object. If you already use Jackson try to implement custom JsonDeserializer instead of custom XmlAdapter. We can simplify our custom deserialiser by deserialising internal Participant objects to Map. Simple example:
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.type.MapType;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
public class XmlMapperApp {
public static void main(String[] args) throws Exception {
File xmlFile = new File("./resource/test.xml").getAbsoluteFile();
XmlMapper xmlMapper = new XmlMapper();
Participants result = xmlMapper.readValue(xmlFile, Participants.class);
System.out.println(result);
}
}
class ParticipantsXmlAdapter extends JsonDeserializer<Participants> {
#Override
public Participants deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
List<Map<String, Object>> participants = readParticipantsMap(p, ctxt);
Participants result = new Participants();
for (Map<String, Object> participantMap : participants) {
Object side = participantMap.get("side").toString();
if ("AWAY".equals(side)) {
result.setParticipantAway(convert((Map<String, Object>) participantMap.get("team")));
} else if ("HOME".equals(side)) {
result.setParticipantHome(convert((Map<String, Object>) participantMap.get("team")));
}
}
return result;
}
private List<Map<String, Object>> readParticipantsMap(JsonParser p, DeserializationContext ctxt) throws IOException {
MapType mapType = ctxt.getTypeFactory().constructMapType(Map.class, String.class, Object.class);
JsonDeserializer<Object> mapDeserializer = ctxt.findRootValueDeserializer(mapType);
List<Map<String, Object>> participants = new ArrayList<>();
p.nextToken(); // skip Start of Participants object
while (p.currentToken() == JsonToken.FIELD_NAME) {
p.nextToken(); // skip start of Participant
Object participant = mapDeserializer.deserialize(p, ctxt);
participants.add((Map<String, Object>) participant);
p.nextToken(); // skip end of Participant
}
return participants;
}
private Participant convert(Map<String, Object> map) {
Participant participant = new Participant();
participant.setId(Integer.parseInt(map.get("id").toString()));
participant.setName(map.get("name").toString());
return participant;
}
}
#JsonDeserialize(using = ParticipantsXmlAdapter.class)
class Participants {
private Participant participantHome;
private Participant participantAway;
// getters, setters, toString
}
class Participant {
private int id;
private String name;
// getters, setters, toString
}
prints:
Participants{participantHome=Participant{id=18594, name='Memphis Express'}, participantAway=Participant{id=18591, name='Orlando Apollos'}}
You can use List of Participant in place of two different participant.
Annotate side with #XmlAttribute(name = "side", required = true).
Then create two different Participant objects and add them to list.
Couple of great answers and alternatives here but I've decided to go with a hybrid of binding with a list and returning the correct home or away team by implementing getter methods that return the correct home or away team to essentially flatten the List. This will reduce the amount of computation when processing the lists throughout the application.
I added the following code to my parent class (to each home/away participant):
Participant getHome() {
return (Participant) participants.stream()
.filter(p -> p.getSide().equalsIgnoreCase("home"));
}
Participant getAway() {
return (Participant) participants.stream()
.filter(p -> p.getSide().equalsIgnoreCase("away"));
}
Thanks for the help!
Related
Here my POJO:
public class AutorDenormalized {
private String id;
private Long unitatId;
private String grupId;
private String descripcio;
public AutorDenormalized() {
}
// getters $ setters
}
I'd like to serialise this kind of objects adding a suffix according to field type. I mean,
If field type is a String -> then add a *_s suffix
If field type is a Long -> then add a *_l suffix
Otherwise keep going
Do you have any ideas how to solve it?
You need to implement custom BeanPropertyWriter which can generate property name with a suffix. To register custom BeanPropertyWriter you need to create custom BeanSerializerModifier.
Below example shows simplified implementation which shows a way how to achieve above result:
import com.fasterxml.jackson.databind.BeanDescription;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationConfig;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.BeanPropertyWriter;
import com.fasterxml.jackson.databind.ser.BeanSerializerModifier;
import com.fasterxml.jackson.databind.util.NameTransformer;
import java.io.IOException;
import java.util.List;
public class JsonTypeInfoApp {
public static void main(String[] args) throws IOException {
SimpleModule typeSuffixModule = new SimpleModule();
typeSuffixModule.setSerializerModifier(new TypeSuffixBeanSerializerModifier());
ObjectMapper mapper = new ObjectMapper();
mapper.enable(SerializationFeature.INDENT_OUTPUT);
mapper.registerModule(typeSuffixModule);
System.out.println(mapper.writeValueAsString(new AutorDenormalized()));
}
}
class TypeSuffixBeanSerializerModifier extends BeanSerializerModifier {
#Override
public List<BeanPropertyWriter> changeProperties(SerializationConfig config, BeanDescription beanDesc, List<BeanPropertyWriter> beanProperties) {
for (int i = 0; i < beanProperties.size(); ++i) {
final BeanPropertyWriter writer = beanProperties.get(i);
Class<?> rawType = writer.getType().getRawClass();
if (supports(rawType)) {
final String suffix = constructSuffix(rawType);
beanProperties.set(i, writer.rename(NameTransformer.simpleTransformer(null, suffix)));
}
}
return beanProperties;
}
private String constructSuffix(Class<?> rawType) {
return "_" + Character.toLowerCase(rawType.getSimpleName().charAt(0));
}
private boolean supports(Class<?> rawClass) {
return rawClass == String.class || rawClass == Long.class;
}
}
Above code prints:
{
"id_s" : "1",
"unitatId_l" : 123,
"grupId_s" : "2",
"descripcio_s" : "3"
}
See also:
Jackson custom serialization and deserialization
Aside from the accepted answer, which works fine, you could also consider implementing PropertyNameStrategy: it would let you rename properties and gets field, setter/getter, creator parameter (which you need to find type of property). Might be little bit less work.
I am serializing a class that includes an unmodifiable list with default typing enabled. The problem is that the type that Jackson uses is
java.util.Collections$UnmodifiableRandomAccessList
which, for some reason, the deserializer does not know how to handle.
Is there a way to tell Jackson to set the type as
java.util.ArrayList
which the deserializer does know how to handle, instead? If possible, I'd like to do it using mixins.
Something like
public abstract class ObjectMixin {
#JsonCreator
public ObjectMixin(
#JsonProperty("id") String id,
#JsonProperty("list") #JsonSerialize(as = ArrayList.class) List<String> list;
) {}
}
which, unfortunately, does not work.
I would like to start from security risk warning which comes from ObjectMapper documentation:
Notes on security: use "default typing" feature (see
enableDefaultTyping()) is a potential security risk, if used with
untrusted content (content generated by untrusted external parties).
If so, you may want to construct a custom TypeResolverBuilder
implementation to limit possible types to instantiate, (using
setDefaultTyping(com.fasterxml.jackson.databind.jsontype.TypeResolverBuilder<?)).
Lets implement custom resolver:
class CollectionsDefaultTypeResolverBuilder extends ObjectMapper.DefaultTypeResolverBuilder {
private final Map<String, String> notValid2ValidIds = new HashMap<>();
public CollectionsDefaultTypeResolverBuilder() {
super(ObjectMapper.DefaultTyping.OBJECT_AND_NON_CONCRETE);
this._idType = JsonTypeInfo.Id.CLASS;
this._includeAs = JsonTypeInfo.As.PROPERTY;
notValid2ValidIds.put("java.util.Collections$UnmodifiableRandomAccessList", ArrayList.class.getName());
// add more here...
}
#Override
protected TypeIdResolver idResolver(MapperConfig<?> config, JavaType baseType, Collection<NamedType> subtypes,
boolean forSer, boolean forDeser) {
return new ClassNameIdResolver(baseType, config.getTypeFactory()) {
#Override
protected String _idFrom(Object value, Class<?> cls, TypeFactory typeFactory) {
String id = notValid2ValidIds.get(cls.getName());
if (id != null) {
return id;
}
return super._idFrom(value, cls, typeFactory);
}
};
}
}
Now, we can use it as below:
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.cfg.MapperConfig;
import com.fasterxml.jackson.databind.jsontype.NamedType;
import com.fasterxml.jackson.databind.jsontype.TypeIdResolver;
import com.fasterxml.jackson.databind.jsontype.impl.ClassNameIdResolver;
import com.fasterxml.jackson.databind.type.TypeFactory;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class JsonApp {
public static void main(String[] args) throws Exception {
ObjectMapper mapper = new ObjectMapper();
mapper.enable(SerializationFeature.INDENT_OUTPUT);
mapper.setDefaultTyping(new CollectionsDefaultTypeResolverBuilder());
Root root = new Root();
root.setData(Collections.unmodifiableList(Arrays.asList("1", "b")));
String json = mapper.writeValueAsString(root);
System.out.println(json);
System.out.println(mapper.readValue(json, Root.class));
}
}
class Root {
private List<String> data;
public List<String> getData() {
return data;
}
public void setData(List<String> data) {
this.data = data;
}
#Override
public String toString() {
return "Root{" +
"data=" + data +
'}';
}
}
Above code prints:
{
"data" : [ "java.util.ArrayList", [ "1", "b" ] ]
}
Root{data=[1, b]}
You can even map it to List interface:
notValid2ValidIds.put("java.util.Collections$UnmodifiableRandomAccessList", List.class.getName());
And output would be:
{
"data" : [ "java.util.List", [ "1", "b" ] ]
}
I am trying to mask sensitive data while serializing using jackson.
I have tried using #JsonSerialize and a custom annotation #Mask .
Mask.java
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
#Retention(RetentionPolicy.RUNTIME)
#Target(ElementType.FIELD)
public #interface Mask {
String value() default "XXX-DEFAULT MASK FORMAT-XXX";
}
Employee.java
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import java.util.Map;
public class Employee {
#Mask(value = "*** The value of this attribute is masked for security reason ***")
#JsonSerialize(using = MaskStringValueSerializer.class)
protected String name;
#Mask
#JsonSerialize(using = MaskStringValueSerializer.class)
protected String empId;
#JsonSerialize(using = MaskMapStringValueSerializer.class)
protected Map<Category, String> categoryMap;
public Employee() {
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getEmpId() {
return empId;
}
public void setEmpId(String empId) {
this.empId = empId;
}
public Map<Category, String> getCategoryMap() {
return categoryMap;
}
public void setCategoryMap(Map<Category, String> categoryMap) {
this.categoryMap = categoryMap;
}
}
Category.java
public enum Category {
#Mask
CATEGORY1,
#Mask(value = "*** This value of this attribute is masked for security reason ***")
CATEGORY2,
CATEGORY3;
}
MaskMapStringValueSerializer.java
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import java.io.IOException;
import java.util.Map;
public class MaskMapStringValueSerializer extends JsonSerializer<Map<Category, String>> {
#Override
public void serialize(Map<Category, String> map, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
jsonGenerator.writeStartObject();
for (Category key : map.keySet()) {
Mask annot = null;
try {
annot = key.getClass().getField(key.name()).getAnnotation(Mask.class);
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
if (annot != null) {
jsonGenerator.writeStringField(((Category) key).name(), annot.value());
} else {
jsonGenerator.writeObjectField(((Category) key).name(), map.get(key));
}
}
jsonGenerator.writeEndObject();
}
}
MaskStringValueSerializer.java
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.BeanProperty;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ser.ContextualSerializer;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
import java.io.IOException;
public class MaskStringValueSerializer extends StdSerializer<String> implements ContextualSerializer {
private Mask annot;
public MaskStringValueSerializer() {
super(String.class);
}
public MaskStringValueSerializer(Mask logMaskAnnotation) {
super(String.class);
this.annot = logMaskAnnotation;
}
public void serialize(String s, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
if (annot != null && s != null && !s.isEmpty()) {
jsonGenerator.writeString(annot.value());
} else {
jsonGenerator.writeString(s);
}
}
public JsonSerializer<?> createContextual(SerializerProvider serializerProvider, BeanProperty beanProperty) throws JsonMappingException {
Mask annot = null;
if (beanProperty != null) {
annot = beanProperty.getAnnotation(Mask.class);
}
return new MaskStringValueSerializer(annot);
}
}
MaskValueTest.java
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.HashMap;
import java.util.Map;
public class MaskValueTest {
public static void main(String args[]) throws Exception{
Employee employee = new Employee();
employee.setName("John Doe");
employee.setEmpId("1234567890");
Map<Category, String> catMap = new HashMap<>();
catMap.put(Category.CATEGORY1, "CATEGORY1");
catMap.put(Category.CATEGORY2, "CATEGORY2");
catMap.put(Category.CATEGORY3, "CATEGORY3");
employee.setCategoryMap(catMap);
ObjectMapper mapper = new ObjectMapper();
System.out.println(mapper.writerWithDefaultPrettyPrinter().writeValueAsString(employee));
}
}
Output -
{
"name" : "*** The value of this attribute is masked for security reason ***",
"empId" : "XXX-DEFAULT MASK FORMAT-XXX",
"categoryMap" : {
"CATEGORY1" : "XXX-DEFAULT MASK FORMAT-XXX",
"CATEGORY2" : "*** The value of this attribute is masked for security reason ***",
"CATEGORY3" : "CATEGORY3"
}
}
The result is as per expectation, however, this seems to be static masking.
The intention was to mask only when needed, e.g. while printing in the logs where the all these sensitive data should be masked.
If I have to send this json for document indexing where the values should be as it is, this implementation fails.
I am looking for an Annotation based solution, where I can use 2 different instance of ObjectMapper initialized with JsonSerializers.
This can be an implementation for what Andreas suggested:
create a class MaskAnnotationIntrospector which extend from JacksonAnnotationIntrospector and override its findSerializer method, like this:
public class MaskAnnotationIntrospector extends JacksonAnnotationIntrospector {
#Override
public Object findSerializer(Annotated am) {
Mask annotation = am.getAnnotation(Mask.class);
if (annotation != null)
return MaskingSerializer.class;
return super.findSerializer(am);
}
}
Therefore, you can have two instance of ObjectMapper. Add MaskAnnotationIntrospector to the one in which you want to Mask (e.g. for logging purpose):
mapper.setAnnotationIntrospector(new MaskAnnotationIntrospector());
The other instance which MaskAnnotationIntrospector has not set into it, do not mask any during serialization.
P.S. MaskAnnotationIntrospector can be extended from both JacksonAnnotationIntrospector & NopAnnotationIntrospector, but the latter does not provide any implementation for findSerializer method and calling super.findSerializer(am) simply return null and as a direct result, other Jackson annotation (such as #JsonIgnore) discarded, but by using the former, this problem solved
Remove the #JsonSerialize annotations, and put the logic of how to handle the #Mask annotation in a Module, e.g. have it add an AnnotationIntrospector.
You can now choose whether or not to call registerModule(Module module).
As for writing the module, I'll leave that up to you. If you have any questions about that, ask another Question.
Instead of having MaskStringValueSerializer.java you can create module to bundle the serializer and register the module with objectmapper whenever you want , which will eventually allow you to have two different instances of objectmapper.
Create a module to bundle the serializer
public class MaskingModule extends SimpleModule {
private static final String NAME = "CustomIntervalModule";
private static final VersionUtil VERSION_UTIL = new VersionUtil() {};
public MaskingModule() {
super(NAME, VERSION_UTIL.version());
addSerializer(MyBean.class, new MaskMapStringValueSerializer());
}
}
Register the module with ObjectMapper and use it
ObjectMapper objectMapper = new ObjectMapper().registerModule(new MaskingModule());
System.out.println(objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(employee));
Also you can extend the Object Mapper , register the module and use it
public class CustomObjectMapper extends ObjectMapper {
public CustomObjectMapper() {
registerModule(new MaskingModule());
}
}
CustomObjectMapper customObjectMapper = new CustomObjectMapper ();
System.out.println(customObjectMapper .writerWithDefaultPrettyPrinter().writeValueAsString(employee));
why don't you use two parameters one for original value and one for masked value. For example in this case you can use String name and String maskedName. then for logging you can use masked value.
I have an object that sometimes looks like this:
{
"foo" : "bar",
"fuzz" : "bla"
}
and sometimes looks like this:
{
"foo" : { "value" : "bar", "baz": "asdf" },
"fuzz" : { "thing" : "bla", "blip" : "asdf" }
}
these classes would look like:
public class Foo {
String value;
String baz;
}
public class Fuzz {
String thing;
String blip;
}
where the first cases are shorthand for the second ones. I would like to always deserialize into the second case.
Further - this is a pretty common pattern in our code, so I would like to be able to do the serialization in a generic manner, as there are other classes similar to Foo above that have the same pattern of using String as a syntactic sugar for a more complex object.
I'd imagine the code to use it would look something like this
public class Thing {
#JsonProperty("fuzz")
Fuzz fuzz;
#JsonProperty("foo")
Foo foo;
}
How do I write a custom deserializer (or some other module) that generically handles both cases?
To make it generic we need to be able to specify name which we would like to set in object for JSON primitive. Some flexibility gives annotation approach. Let's define simple annotation:
#Retention(RetentionPolicy.RUNTIME)
#Target(ElementType.FIELD)
#interface JsonPrimitiveName {
String value();
}
Name means: in case primitive will appear in JSON use value() to get property name for given primitive. It binds JSON primitive with POJO field. Simple deserialiser which handles JSON object and JSON primitive:
class PrimitiveOrPojoJsonDeserializer extends JsonDeserializer implements ContextualDeserializer {
private String primitiveName;
private JavaType type;
#Override
public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
JsonDeserializer<Object> deserializer = ctxt.findRootValueDeserializer(type);
if (p.currentToken() == JsonToken.START_OBJECT) {
return deserializer.deserialize(p, ctxt);
} else if (p.currentToken() == JsonToken.VALUE_STRING) {
BeanDeserializer beanDeserializer = (BeanDeserializer) deserializer;
try {
Object instance = beanDeserializer.getValueInstantiator().getDefaultCreator().call();
SettableBeanProperty property = beanDeserializer.findProperty(primitiveName);
property.deserializeAndSet(p, ctxt, instance);
return instance;
} catch (Exception e) {
throw JsonMappingException.from(p, e.getMessage());
}
}
return null;
}
#Override
public JsonDeserializer<?> createContextual(DeserializationContext ctxt, BeanProperty property) {
JsonPrimitiveName annotation = property.getAnnotation(JsonPrimitiveName.class);
PrimitiveOrPojoJsonDeserializer deserializer = new PrimitiveOrPojoJsonDeserializer();
deserializer.primitiveName = annotation.value();
deserializer.type = property.getType();
return deserializer;
}
}
Now we need to annotate POJO fields as below:
class Root {
#JsonPrimitiveName("value")
#JsonDeserialize(using = PrimitiveOrPojoJsonDeserializer.class)
private Foo foo;
#JsonPrimitiveName("thing")
#JsonDeserialize(using = PrimitiveOrPojoJsonDeserializer.class)
private Fuzz fuzz;
// getters, setters
}
I assume that all classes are POJO-s and follow all rules - have getters, setters and default constructor. In case constructor does not exist you need to change this beanDeserializer.getValueInstantiator().getDefaultCreator().call() line somehow which fits your requirements.
Example app:
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.databind.BeanProperty;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.deser.BeanDeserializer;
import com.fasterxml.jackson.databind.deser.ContextualDeserializer;
import com.fasterxml.jackson.databind.deser.SettableBeanProperty;
import java.io.File;
import java.io.IOException;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
public class JsonApp {
public static void main(String[] args) throws Exception {
File jsonFile = new File("./resource/test.json").getAbsoluteFile();
ObjectMapper mapper = new ObjectMapper();
System.out.println(mapper.readValue(jsonFile, Root.class));
}
}
Prints for shortened JSON:
Root{foo=Foo{value='bar', baz='null'}, fuzz=Fuzz{thing='bla', blip='null'}}
And for full JSON payload:
Root{foo=Foo{value='bar', baz='asdf'}, fuzz=Fuzz{thing='bla', blip='asdf'}}
I was just wondering, given a pojo:
public class MyProfileDto {
private List<String> skills;
//mutators; getSkills; setSkills + bunch of other fields
}
and JSON for the skills field:
"skills":{
"values":[
{
"id":14,
"skill":{
"name":"C++"
}
},
{
"id":15,
"skill":{
"name":"Java"
}
}
],
"_total":2
}
Is there any way using Jackson to get the skills/values/skill/name field (i.e. "Java", "C++") into the target Dto's String List without creating a custom deserializer for the entire Dto? It has many fields so an ideal solution would involve some custom annotation or deserializer for the one field if possible??
Jackson does not contains any XPath feature but you can define converter for each property. This converter will be used by Jackson to convert input type to output type which you need. In your example input type is Map<String, Object> and output type is List<String>. Probably, this is not the simplest and the best solution which we can use but it allows us to define converter for only one property without defining deserializer for entire entity.
Your POJO class:
class MyProfileDto {
#JsonDeserialize(converter = SkillConverter.class)
private List<String> skills;
public List<String> getSkills() {
return skills;
}
public void setSkills(List<String> skills) {
this.skills = skills;
}
}
Converter for List<String> skills; property:
class SkillConverter implements Converter<Map<String, Object>, List<String>> {
#SuppressWarnings("unchecked")
public List<String> convert(Map<String, Object> value) {
Object values = value.get("values");
if (values == null || !(values instanceof List)) {
return Collections.emptyList();
}
List<String> result = new ArrayList<String>();
for (Object item : (List<Object>) values) {
Map<String, Object> mapItem = (Map<String, Object>) item;
Map<String, Object> skillMap = (Map<String, Object>) mapItem.get("skill");
if (skillMap == null) {
continue;
}
result.add(skillMap.get("name").toString());
}
return result;
}
public JavaType getInputType(TypeFactory typeFactory) {
return typeFactory.constructMapLikeType(Map.class, String.class, Object.class);
}
public JavaType getOutputType(TypeFactory typeFactory) {
return typeFactory.constructCollectionLikeType(List.class, String.class);
}
}
And example usage:
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.type.TypeFactory;
import com.fasterxml.jackson.databind.util.Converter;
public class JacksonProgram {
public static void main(String[] args) throws IOException {
ObjectMapper mapper = new ObjectMapper();
MyProfileDto dto = mapper.readValue(new File("/x/json"), MyProfileDto.class);
System.out.println(dto.getSkills());
}
}
Above program prints:
[C++, Java]