Passing parameters to custom Deserializer - java

I have json that contains multiple rows.
{
"rows": [{
"aId": 408,
"aFrstNm": "TIM",
},
{
"aId": 410,
"aFrstNm": "BOB",
},
{
"aId": 409,
"aFrstNm": "HENRY",
}]
}
and POJOs
public class User extends Employee {
#JsonProperty("tableName")
private Double aID;
#JsonProperty("aFrstNm")
private String aFrstNm;
getters and setters ommitted
}
public class Employee {
}
public class Employees {
private Collection<? extends Employee> rows;
getters and setters ommitted
}
What I am trying to do is to pass the User class to a custom deserializer to be able return employees as a collection of Users that extend Employee:
SimpleModule simpleModule = new SimpleModule("MyModule")
.addDeserializer(Employees.class, new JsonDeserializer<Employees>() {
#Override
public Employees deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
ObjectMapper mapper = (ObjectMapper) p.getCodec();
final Class<?> clazz = (Class<?>) ctxt.getAttribute("mappingClass");
final JsonNode jsonNode = (JsonNode) mapper.readTree(p).get("rows");
Employees employees = new Employees();
Collection<Product> rows = new ArrayList();
if (jsonNode.isArray()) for (final JsonNode objNode : jsonNode) {
boolean isValueNode = objNode.isValueNode();
String json = mapper.writeValueAsString(objNode);
Product product = (Product) new ObjectMapper().readValue(json, clazz);
rows.add(product);
System.out.println(jsonNode);
}
employees.setRows(rows);
return employees;
}
});
mapper.registerModule(simpleModule);
the variable clazz (final Class clazz) attribute should contain the class a parameter. I so far have been unable to find a mechanism that allows this. DeserializationContext seems to be promising but I have not been able to make this work. Any suggestions on using DeserializationContext or some other way to pass parameters?
Tnx.

Related

How to Deserialize missing properties to default values with jackson in java

I am trying to make jackson deserialize missing properties that represent collections into empty collections instead of NULL. I've tried a few different things and this is the latest that isn't working.
If there is a way to make any potential solution globally configured for all POJO deserialization that would be great.
Given the following json, which is missing the property 'assets':
{
"name":"my-layer",
"code": "ly1",
"types":["type1", "type2"],
"private": false
}
Given the following POJO:
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonSetter;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Builder.Default;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
#Data
#Builder
#AllArgsConstructor
#NoArgsConstructor
#EqualsAndHashCode
public class CreateLayerRequest {
#NotBlank
#JsonProperty(required = true)
private String name;
#NotBlank
#JsonProperty(required = true)
private String code;
#Default
#NotNull
#JsonProperty(value = "private", required = true)
private Boolean privateLayer = Boolean.FALSE;
#Default
#JsonSetter(nulls = Nulls.AS_EMPTY)
#JsonProperty("types")
private Set<UUID> types = new HashSet<>();
#Default
#JsonSetter(nulls = Nulls.AS_EMPTY)
#JsonProperty("assets")
private Set<UUID> assets = new HashSet<>();
}
configured my fix to be global
#Configuration
public class MyAppConfiguration implements WebMvcConfigurer {
/**
* allows POJOs using json filtering to serialize all properties when not being filtered
*/
#Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
for (HttpMessageConverter<?> converter : converters) {
if (converter instanceof MappingJackson2HttpMessageConverter) {
((MappingJackson2HttpMessageConverter) converter)
.getObjectMapper()
// not related to this thread, this is for property filtering when serializing POJOs
.setFilterProvider(new SimpleFilterProvider().setFailOnUnknownId(false))
// these modules are related to to this thread
.registerModule(
new SimpleModule().addDeserializer(Set.class, new CollectionDeserializer())
)
.registerModule(
new SimpleModule().addDeserializer(List.class, new CollectionDeserializer())
)
.registerModule(
new SimpleModule().addDeserializer(Map.class, new CustomMapDeserializer())
);
}
}
}
#Slf4j
#NoArgsConstructor
#AllArgsConstructor
private static class CollectionDeserializer<T>
extends JsonDeserializer<Collection<T>>
implements ContextualDeserializer {
private JavaType type;
private final ObjectMapper mapper = new ObjectMapper();
#Override
public JsonDeserializer<?> createContextual(
DeserializationContext deserializationContext,
BeanProperty beanProperty) {
final JavaType javaType = deserializationContext.getContextualType() != null
? deserializationContext.getContextualType()
: beanProperty.getMember().getType();
return new CollectionDeserializer<>(javaType);
}
#Override
public Collection<T> deserialize(
JsonParser jsonParser,
DeserializationContext deserializationContext) throws IOException {
try {
if (Set.class.isAssignableFrom(type.getRawClass())) {
return deserializeJsonToCollection(jsonParser, new HashSet<>());
}
if (List.class.isAssignableFrom(type.getRawClass())) {
return deserializeJsonToCollection(jsonParser, new ArrayList<>());
}
} catch (IllegalArgumentException e) {
log.warn("unable to deserialize array property in request");
throw e;
}
final String message = "unable to deserialize collection";
log.warn(message);
throw new CollectionDeserializationException(message);
}
private Collection<T> deserializeJsonToCollection(
JsonParser jsonParser,
Collection<T> collection) throws IOException {
((ArrayNode) jsonParser
.getCodec()
.readTree(jsonParser))
.forEach(item -> {
try {
final T value = mapper.readValue(
String.format("\"%s\"", item.textValue()),
type.getContentType()
);
collection.add(value);
} catch (IOException e) {
final String message = String.format(
"unable to deserialize value [%s] to type [%]",
item.textValue(),
type.getContentType()
);
log.warn(message);
throw new CollectionDeserializationException(message);
}
});
return collection;
}
#Override
public Collection<T> getNullValue(DeserializationContext ctxt) {
if (Set.class.isAssignableFrom(type.getRawClass())) {
return new HashSet<>();
}
if (List.class.isAssignableFrom(type.getRawClass())) {
return new ArrayList<>();
}
return null;
}
}
#Slf4j
private static class CustomMapDeserializer<T, K> extends JsonDeserializer<Map<T, K>> {
private final ObjectMapper mapper = new ObjectMapper();
private final TypeReference<Map<T, K>> mapTypeReference = new TypeReference<>() {
};
#Override
public Map<T, K> deserialize(
JsonParser jsonParser,
DeserializationContext deserializationContext) {
try {
return mapper.readValue(jsonParser, mapTypeReference);
} catch (Exception e) {
final String message = "unable to deserialize map";
log.warn(message);
throw new MapDeserializationException(message, e);
}
}
#Override
public Map<T, K> getNullValue(DeserializationContext ctxt) {
return new HashMap<>();
}
}

Deserialize JSON array to Map using Jackson

I have JSON structured like:
{
"id" : "123",
"name" : [ {
"stuff" : [ {
"id" : "234",
"name" : "Bob"
}, {
"id" : "345",
"name" : "Sally"
} ]
} ]
}
That I want to map to the following data structure:
MyInterface1
#Value.Immutable
#JsonSerialize(as = ImmutableMyInterface1.class)
#JsonDeserialize(as = ImmutableMyInterface1.class)
public interface MyInterface1 {
String id();
#JsonDeserialize(using = MyInterface1Deserializer.class)
List<MyInterface2> name();
}
MyInterface2
#Value.Immutable
#JsonSerialize(as = ImmutableMyInterface2.class)
#JsonDeserialize(as = ImmutableMyInterface2.class)
public interface MyInterface2 {
#JsonDeserialize(using = StuffDeserializer.class)
Map<String, MyInterface3> stuff();
}
MyInterface3
#Value.Immutable
#JsonSerialize(as = ImmutableMyInterface3.class)
#JsonDeserialize(as = ImmutableMyInterface3.class)
public interface MyInterface3 {
String id();
String name();
}
I'm using an ObjectMapper with readValue(stringWithJson,MyInterface1.class) to map this JSON to MyInterface1, which should continue down the chain to MyInterface3. This setup was working when I was using a List in MyInterface2, i.e. List<MyInterface3> name();
However, I want this to be a map instead of a list, ideally with "id" from the inner JSON as the key. This would allow me to get values with the following syntax:
MyInterface1.get(0).MyInterface2.get("id1").name();
The problem is that when attempting to create a custom StuffDeserializer.class, I'm getting the error:
Can not deserialize instance of com.foo.ImmutableMyInterface2$Json out of START_ARRAY token
when trying to do:
public Map<String, MyInterface3> deserialize(JsonParser jsonParser, DeserializationContext ctxt)
throws IOException {
MyInterface2 foo = Unmarshaller.OBJECT_MAPPER.readValue(jsonParser, MyInterface2.class); // error here
...
I think this is because Jackson is expecting "stuff" to be a List 'cause of the JSON array. What's the best way to deserialize this JSON to a map that uses values from the inner JSON as a key?
I would create a custom JsonDeserializer to map id and name into a map:
public class StringHashMapValueDeserializer extends JsonDeserializer<HashMap<String, String>>{
#Override
public HashMap<String, String> deserialize(JsonParser parser, DeserializationContext ctxt)
throws IOException, JsonProcessingException {
HashMap<String, String> ret = new HashMap<String, String>();
ObjectCodec codec = parser.getCodec();
TreeNode node = codec.readTree(parser);
if (node.isArray()){
for (JsonNode n : (ArrayNode)node){
JsonNode id = n.get("id");
if (id != null){
JsonNode name = n.get("name");
ret.put(id.asText(), name.asText());
}
}
}
return ret;
}
}
And then I would create simple beans with annotating stuff property with the deserializer:
#Getter
#Setter
public class Name {
#JsonDeserialize(using = StringHashMapValueDeserializer.class)
Map<String, String> stuff;
#Override
public String toString() {
return "Name [stuff=" + stuff + "]";
}
}
Outer type:
#Getter
#Setter
public class OuterType {
String id;
List<Name> name;
#Override
public String toString() {
return "OuterType [id=" + id + ", stuff=" + name + "]";
}
}
Deserialization:
ObjectMapper mapper = new ObjectMapper();
OuterType response;
response = mapper.readValue(json, OuterType.class);
System.out.println(response);
System.out.println(response.getName().get(0).getStuff().get("234"));
console output:
OuterType [id=123, stuff=[Name [stuff={234=Bob, 345=Sally}]]]
Bob
Hope it helps.

How to deserialize a list as a single objects instead of a list object?

I have the following Address and AddressList classes
public class Address {
private String street;
private String city;
private String state;
// ...
}
public class AddressList {
private List<Address> addresses;
// ...
}
and then a Person class
public class Person {
private String name;
#JsonDeserialize(contentUsing = ListDeserializer.class)
private Map<String, AddressList> addresses;
// ..
}
Then I have the Yaml file like this
---
name: 'abc'
addresses:
offices:
- street: 123 main st
city: san francisco
state: ca
- street: 234 post st
city: san francisco
state: ca
My List deserializer class is as follows:
public class ListDeserializer extends JsonDeserializer<AddressList> {
#Override
public AddressList deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JsonProcessingException {
return jsonParser.readValueAs(new TypeReference<Map<String, List<Address>>>() {
});
}
}
my parsing code is as follows:
ObjectMapper objectMapper = new ObjectMapper(new YAMLFactory());
return objectMapper.readValue(inputYamlFile, Person.class);
when I read the list of addresses: it prints as
{offices=null}
Can anyone help with resolving this?
If it was serialized as a list you have to deserialize it the same way. But you then have the list and and can iterate it yourself.
I found a way to resolve this issue using deserializing addresses map using Converter.
The modifications I made are
public class Person {
private String name;
#JsonDeserialize(converter = AddressListConverter.class)
private Map<String, AddressList> addresses;
// ..
}
I, then, wrote a new converter class for the AddressList.
public class AddressListConverter implements Converter<Map<String, List<LinkedHashMap>>, Map<String, AddressList>> {
#Override
public Map<String, AddressList> convert(Map<String, List<LinkedHashMap>> stringListMap) {
Map<String, AddressList> addressListMap = new HashMap<>();
ObjectMapper mapper = new ObjectMapper();
for (Map.Entry<String, List<LinkedHashMap>> entry : stringListMap.entrySet()) {
AddressList addressList = new AddressList();
for(LinkedHashMap map: entry.getValue()) {
Address address = mapper.convert(map, Address.class);
addressList.getAddresses().add(address);
}
addressListMap.put(entry.getKey(), addressList);
}
return addressListMap;
}
#Override
public JavaType getInputType(TypeFactory typeFactory) {
return typeFactory.constructMapType(Map.class, String.class, List.class);
}
#Override
public JavaType getOutputType(TypeFactory typeFactory) {
return typeFactory.constructMapType(Map.class, String.class, AddressList.class);
}
}
This should do the trick

Custom object display in JSON

I'm using only jersey and not jackson to create a REST api. I've two model objects,
public class Course {
private int id;
private String name;
private Teacher teacher;
}
public class Teacher {
private int id;
private String givenName;
private String familyName;
}
I'm creating a service and returning a List of Course objects,
public List<Course> getAll(){
return db.getCourseList();
}
The display is as expected,
[{"id":101,"name":"Introduction to Java","teacher":{"familyName":"Bar","givenName":"Foo","id":201}},{"id":102,"name":"Intermediate Java","teacher":{"familyName":"Prank","givenName":"Foo","id":202}}]
Now I want to customize my JSON object to display in the following format, with only the teacher ID.
[{"id":"100","name":"Introduction to Java","teacherId":"201"},{"id":"101","name":"Intermediate Java","teacherId":"201"}
So this is the view model that I designed.
#XmlRootElement
public class CourseTeacherIdView {
private int id;
private String name;
private int teacherId;
CourseTeacherIdView(){
}
public CourseTeacherIdView(int id, String name, int teacherId){
this.id = id;
this.name = name;
this.teacherId = teacherId;
}
}
And I use this method to return the List of view objects.
public List<CourseTeacherIdView> getAll(){
List<Course> list = db.getCourseList();
List<CourseTeacherIdView> viewList = new ArrayList<>();
for(Iterator<Course> itr = list.iterator(); itr.hasNext();){
Course c = (Course) itr.next();
viewList.add(new CourseTeacherIdView(c.getId(), c.getName(), c.getTeacher().getId()));
}
return viewList;
}
This is the result that I get.
[{},{},{}]
What am I doing wrong.
You can achieve that with Jackson and creating a custom serializer like the following:
public class CourseSerializer extends JsonSerializer<Course> {
#Override
public void serialize(Course value,
JsonGenerator gen,
SerializerProvider serializers) throws IOException {
gen.writeStartObject();
Field[] fields = value.getClass().getDeclaredFields();
for (Field field : fields) {
field.setAccessible(true);
try {
Object obj = field.get(value);
if (obj instanceof Teacher) {
Teacher teacher = (Teacher) obj;
gen.writeStringField("teacherId", String.valueOf(teacher.getId()));
} else {
gen.writeStringField(field.getName(), obj.toString());
}
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
gen.writeEndObject();
}
}
Test case:
public static void main(String[] args) throws Exception {
ObjectMapper mapper = new ObjectMapper();
SimpleModule module = new SimpleModule();
module.addSerializer(Course.class, new CourseSerializer());
mapper.registerModule(module);
Teacher teacher1 = new Teacher(123, "teacher1", "surename1");
Teacher teacher2 = new Teacher(234, "teacher2", "surename2");
Course course1 = new Course(1, "course1", teacher1);
Course course2 = new Course(2, "course2", teacher2);
List<Course> courses = Arrays.asList(new Course[]{course1, course2});
String serialized = mapper.writeValueAsString(courses);
}
Output:
[{
"id": "1",
"name": "course1",
"teacherId": "123"
}, {
"id": "2",
"name": "course2",
"teacherId": "234"
}]
If I understood you correctly you could either create new view model representation with only the id and map each object in the list to it, or use #jsonignore on not relevant fields (if jersey is using Jackson). Or even retrieve only the ids from the db. Depends on use case.

How to serialize JSON array as a numbered/indexed JSON using jackson?

I have an array of objects of following class
public class Person
{
private Long id;
private String name;
//Rest of the getters and setters
}
public class Data
{
private ArrayList<Person> persons;
public Data()
{
persons = new ArrayList<Person>();
Person p1 = new Person(1L, "walter");
Person p2 = new Person(2L, "white");
persons.add(p1);
persons.add(p2);
}
}
Now if I create the object Data in my program and serialize this Data object using jackson, it will give me the following JSON.
{
"data": {
"persons": [
{
"id": 1,
"name": "walter"
},
{
"id": 2,
"name": "white"
}
]
}
}
is there any way to serialize this object into following numbered/indexed JSON?
{
"data": {
"persons": {
"1": {
"id": 1,
"name": "walter"
},
"2": {
"id": 2,
"name": "white"
}
}
}
}
I don't think that is something you can just tell Jackson to do.
The easiest solution I can think of is to turn the array you get into a HashMap<Integer, Person> and pass that into jackson.
If order matters (you want "1" to be before "2") then you can use a LinkedHashMap<Integer, Person>
Something like this:
HashMap<Integer, Person> pMap = new HashMap<>();
for(int i = 0; i < persons.size(); i++){
pMap.put(i, persons.get(i));
}
Edit:
Turns out you can do it, you just have to write the serializer yourself1, so you would do something like:
public class PersonSerializer extends JsonSerializer<Person> {
#Override
public void serialize(Person person, JsonGenerator jgen, SerializerProvider provider)
throws IOException, JsonProcessingException {
jgen.writeStartObject(String.valueOf(i));
jgen.writeNumberField("id", i);
jgen.writeStringField("name", person.getName());
jgen.writeEndObject();
}
}
You can tell jackson to use this serializer by simply adding an antotation to the Person class
#JsonSerialize(using = PersonSerializer.class)
public class Person
{
private Long id;
private String name;
//Rest of the getters and setters
}

Categories

Resources