Custom Deserialiser to parse complex Json - java

I have to deserialise below Json
{
"Student": [
{
"Number": "12345678",
"Name": "abc"
"Country": "IN",
"AreaOfInterest": [
{
“FootBall”: “Yes”,
“Cricket”: “No”
}
]
}
],
"hasMore": false,
"links": [
{
"rel": "self",
"kind": "collection"
}
]
}
into below POJO
class {
private String number;
private String name;
private String footBall;
}
I have written Gson custom deserialiser to lift up AreaOfInterest as below
public List<? extends Student> deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
var jsonObject = json.getAsJsonObject();
Stream<JsonElement> student = StreamSupport.stream(jsonObject.getAsJsonArray("Student").spliterator(), true);
Stream<JsonElement> areaOfInterest = StreamSupport.stream(jsonObject.getAsJsonArray("Student").get(0).getAsJsonObject().get("AreaOfInterest").getAsJsonArray().spliterator(), true);
Stream.concat(student,areaOfInterest)
.map(it -> context.deserialize(it, Student.class))
.map(Student.class::cast)
.collect(List.collector())
}
But deserialiser returning two objects of Student instead of one, one is all fields are null except footBall other is actual student except footBall as null, any help how to get single object with all the fields will be of great help, thanks in advance.

This won't be your exact answer, but it might be simpler to use gson to obtain a map and construct your pojo from that map. Alternatively, if you don't like the map, create a pojo that looks like your JSON and map that pojo to the pojo you want.
Background/Reasoning: GSON is the mapper of your choice right now, but might be changed to something else, eg. Jackson, and all of your custom, framework specific mappers will need to be converted/changed if that happens. Using gson to create an object, that looks like the source, and map that to your custom POJO in your controller will make your codes intention clear and your code more resilient to framework changes.

Related

Map with object and list of objects to Json

I have got two main model classes: Customer and Product
public class Customer {
String name;
String surname;
int age;
BigDecimal cash;
}
public class Product {
String name;
Category category;
BigDecimal price;
}
I want to build json file with Map<Customer, List<Product>>
When I write to json file data with my method which works correct - I am sure about this - the json file shows this syntax
{
"Customer{name\u003d\u0027Custo1\u0027, surname\u003d\u0027Surname\u0027, age\u003d18, cash\u003d1200}": [
{
"name": "prod1",
"category": "CLOTHES",
"price": 12000
},
{
"name": "prod2",
"category": "ELECTRONIC",
"price": 15000
}
]
}
Then when i want to read this file, the error Exception in thread "main" java.util.NoSuchElementException: No value present occurs so I think that the Customer syntax from json file is not recognized.
So I tried to write data to json file on my own with this syntax below, but it does not work
[
{
"name": "Abc",
"surname": "Def",
"age": 14,
"cash": "2000"
}
:
[
{
"name": "prod1",
"category": "CLOTHES",
"price": 12000
},
{
"name": "prod2",
"category": "ELECTRONIC",
"price": 15000
}
]
]
json converter method:
public void toJson(final T item) {
try (FileWriter fileWriter = new FileWriter(jsonFilename)) {
fileWriter.write(gson.toJson(item));
} catch (Exception e) {
throw new ValidatorException(e.getMessage());
}
}
#Tom is right on the issues you've faced with. I'll explain why and suggest one more solution.
Your first JSON is technically a valid JSON but it cannot be deserialized, because the map keys are results of the Customer.toString() method Gson uses by default. This is why it looks weird, acts like a debug string, and can't be deserialized back: there it is almost always no way to restore an object from the toString() result (toString is designed mostly for debugging/logging purposes providing basic information regarding the state of a particular object that does not need to expose its all internals at all).
Your second JSON is invalid JSON. Period.
Tom's suggestion of making the list of products a part of the customer class is totally fine. Having it implemented like that lets you to serialize everything as a list like this:
[
{
"name": "john",
"products": [
{"name": "prod1"},
{"name": "prod2"}
]
}
]
Hint: separating domain objects (Customer and Product) and representation objects for data transfer (CustomerDto and ProductDto) is usually a fine idea too since it allows to create representation for any concrete representation implementation (one for various JSON implementation libraries, two for other-format-oriented tools, third for persistence, four for UI views, etc), so it might be implemented like converting Map<Customer, List<Product>> to List<CustomerDto> and back (possibly by using mapper-generators like MapStruct).
If for whatever reason it is not possible to reorganize your domain classes or create Gson-friendly DTO-mappings, or you're fine to keep it as simple as possible and you're fine with having not that trivial JSON structure (as long as you understand implications of the format in this solution: evolution, distribution, etc), then you can enable special Gson mode to support this kind of maps. It generates valid JSONs that can be serialized and deserialized back, but the way it is implemented looks a bit of anti-pattern to me because of losing semantics due to using arrays as the data container.
#AllArgsConstructor
#EqualsAndHashCode
#ToString
final class Customer {
final String name;
}
#AllArgsConstructor
#EqualsAndHashCode
#ToString
final class Product {
final String name;
}
public final class MapTest {
private static final Gson gson = new GsonBuilder()
.enableComplexMapKeySerialization()
.create();
private static final TypeToken<Map<Customer, List<Product>>> customerToProducts = new TypeToken<Map<Customer, List<Product>>>() {};
#Test
public void test() {
final Map<Customer, List<Product>> ordersBefore = ImmutableMap.of(
new Customer("john"), ImmutableList.of(new Product("prod1"), new Product("prod2"))
);
final String json = gson.toJson(ordersBefore, customerToProducts.getType());
Assertions.assertEquals("[[{\"name\":\"john\"},[{\"name\":\"prod1\"},{\"name\":\"prod2\"}]]]", json);
final Map<Customer, List<Product>> ordersAfter = gson.fromJson(json, customerToProducts.getType());
Assertions.assertEquals(ordersBefore, ordersAfter);
}
}
Note that it generates JSON like this (index 0 means the key, index 1 means the value):
[
[
{"name": "john"},
[
{"name": "prod1"},
{"name": "prod2"}
]
]
]

How deserialize json object array as array of json strings?

Consider json input:
{
companies: [
{
"id": 1,
"name": "name1"
},
{
"id": 1,
"name": "name1"
}
],
nextPage: 2
}
How deserialize this into class:
public class MyClass {
List<String> companies;
Integer nextPage;
}
Where List<String> companies; consists of strings:
{"id": 1,"name": "name1"}
{"id": 1,"name": "name1"}
#JsonRawValue doesn't work for List<String> companies;
Is there a way to configure Jackson serialization to keep companies array with raw json string with annotations only? (E.g. without writing custom deserializator)
There is no annotation-only solution for your problem. Somehow you have to convert JSON Object to java.lang.String and you need to specify that conversion.
You can:
Write custom deserializer which is probably most obvious solution but forbidden in question.
Register custom com.fasterxml.jackson.databind.deser.DeserializationProblemHandler and handle com.fasterxml.jackson.databind.exc.MismatchedInputException situation in more sophisticated way.
Implement com.fasterxml.jackson.databind.util.Converter interface and convert JsonNode to String. It is semi-annotational way to solve a problem but we do not implement the worst part - deserialisation.
Let's go to point 2. right away.
2. DeserializationProblemHandler
Solution is pretty simple:
ObjectMapper mapper = new ObjectMapper();
mapper.addHandler(new DeserializationProblemHandler() {
#Override
public Object handleUnexpectedToken(DeserializationContext ctxt, JavaType targetType, JsonToken t, JsonParser p, String failureMsg) throws IOException {
if (targetType.getRawClass() == String.class) {
// read as tree and convert to String
return p.readValueAsTree().toString();
}
return super.handleUnexpectedToken(ctxt, targetType, t, p, failureMsg);
}
});
Read a whole piece of JSON as TreeNode and convert it to String using toString method. Helpfully, toString generates valid JSON. Downside, this solution has a global scope for given ObjectMapper instance.
3. Custom Converter
This solution requires to implement com.fasterxml.jackson.databind.util.Converter interface which converts com.fasterxml.jackson.databind.JsonNode to String:
class JsonNode2StringConverter implements Converter<JsonNode, String> {
#Override
public String convert(JsonNode value) {
return value.toString();
}
#Override
public JavaType getInputType(TypeFactory typeFactory) {
return typeFactory.constructType(new TypeReference<JsonNode>() {
});
}
#Override
public JavaType getOutputType(TypeFactory typeFactory) {
return typeFactory.constructType(new TypeReference<String>() {
});
}
}
and now, you can use annotation like below:
#JsonDeserialize(contentConverter = JsonNode2StringConverter.class)
private List<String> companies;
Solutions 2. and 3. solve this problem almost in the same way - read node and convert it back to JSON, but uses different approaches.
If, you want to avoid deserialising and serialising process you can take a look on solution provided in this article: Deserializing JSON property as String with Jackson and take a look at:
How to serialize JSON with array field to object with String field?
How to get a part of JSON as a plain text using Jackson
How to extract part of the original text from JSON with Jackson?

Deserializing JSON with Jackson for polymorphic classes

I am attempting to deserialize JSON which can be either a GroupRule or AttributeRule:
AbstractRule
GroupRule
AttributeRule
I want my models/entities/POJOs to be generic as I also use the same classes in other projects with Snakeyaml or other serialization providers.
Having said that, I stumbled across this: https://github.com/FasterXML/jackson-docs/wiki/JacksonPolymorphicDeserialization
which in the article, it indicates I could do:
{ // Using fully-qualified path
"#class" : "com.fasterxml.beans.EmployeeImpl", ...
}
However, when I do that, I am getting:
Cannot construct instance of `com.walterjwhite.email.organization.api.configuration.rule.AbstractRule` (no Creators, like default construct, exist): abstract types either need to be mapped to concrete types, have custom deserializer, or contain additional type information
at [Source: (FileInputStream); line: 4, column: 10] (through reference chain: com.walterjwhite.email.organization.api.configuration.rule.EmailMatcherRule["rule"])
com.fasterxml.jackson.databind.exc.InvalidDefinitionException.from(InvalidDefinitionException.java:67)
My configuration is this:
{
"name": "default",
"ordering": "1",
"rule": {
"#class": "com.walterjwhite.email.organization.api.configuration.rule.GroupRule",
"criteriaType": "Should",
"rules": [
{"#class": "com.walterjwhite.email.organization.api.configuration.rule.AttributeRule",
"emailMessageField": "Subject",
"values": ["default"]
}
]
},
"matchType": "ContainsIgnoreCase",
"actionClassNames": [
"com.walterjwhite.email.organization.plugins.count.CountAction",
"com.walterjwhite.email.organization.plugins.index.IndexAction",
"com.walterjwhite.email.organization.plugins.reply.MoveAction"
]
}
On the Java side of things, I am doing this generally:
mapper.readValue(inputStream, entityType);
Now, the entityType in this case is EmailMatcherRule which inside it has a rule field which can either be attribute or group. Inputstream is just the fileInputStream I am passing in ...
I am using Jackson 2.10.1. I also converted the above JSON from YAML which was working fine via Snakeyaml. Note that it automatically embeds the classes into the YAML, so this was a non-issue with it.
Is my JSON correct - according to the documentation, I should be able to add the #class attribute to specify the class I want to use, right?
I tried below and it worked without any configuration. Not sure if thats what you want to achieve:
public static void main(String[] args) throws IOException {
ObjectMapper mapper = new ObjectMapper();
String groupRuleStr = "{\"parentId\":\"parent\",\"groupId\":\"group\"}";
String attributeRuleStr = "{\"parentId\":\"parent\",\"attributeId\":\"attribute\"}";
GroupRule groupRule = mapper.readValue(groupRuleStr, GroupRule.class);
AttributeRule attributeRule = mapper.readValue(attributeRuleStr, AttributeRule.class);
System.out.println(groupRule.groupId);
System.out.println(attributeRule.attributeId);
}
static abstract class AbstractRule {
public String parentId = "parent";
}
static class GroupRule extends AbstractRule {
public String groupId = "group";
}
static class AttributeRule extends AbstractRule {
public String attributeId = "attribute";
}
I had to do this:
objectMapper.activateDefaultTyping(objectMapper.getPolymorphicTypeValidator());
Now, my JSON looks like this (NOTE: this is a different test entity, but you get the idea):
{
"name": "default",
"ordering": "1",
"rule": [
"com.walterjwhite.email.organization.api.configuration.rule.GroupRule",
{
"criteriaType": "Should",
"rules": ["java.util.HashSet",[[
"com.walterjwhite.email.organization.api.configuration.rule.AttributeRule",
{
"emailMessageField": ["com.walterjwhite.email.organization.api.configuration.rule.EmailMessageField", "Subject"],
"values": ["java.util.HashSet", [
"default"
]],
"matchType": ["com.walterjwhite.email.organization.api.configuration.rule.MatchType","ContainsIgnoreCase"]
}]]
]
}
],
"actionClassNames": ["java.util.ArrayList",[
"com.walterjwhite.email.organization.plugins.count.CountAction",
"com.walterjwhite.email.organization.plugins.index.IndexAction",
"com.walterjwhite.email.organization.plugins.reply.MoveAction"
]
]
}
So, the reference documentation I saw with #class seems inaccurate. I am not really happy about adding all this extra information especially when some of it isn't needed - java.util.ArrayList.

Using Jackson, how do you unmarshal JSON to a Java Map, but have a specific nested member as a domain instance?

Given the following example JSON:
{
"aud": "you",
"exp": 1300819380,
"user": {
"name": "Joe",
"address": {
"street": "1234 Main Street",
"city": "Anytown",
"state": "CA",
"postalCode": 94401
}
}
}
and the Java interface:
public interface Deserializer {
Map<String,Object> deserialize(String json);
}
Using Jackson, how do I implement deserialize such that it returns a map of name/value pairs, but converts just the user field into a Java User domain object such that the following is possible:
Map<String,Object> map = deserializer.deserialize(json);
assert map.get("user") instanceof User;
The idea is that the top-level name/value pairs can be anything and are unstructured and we don't want to force a domain model. But if a user member exists, we do want that member value to be unmarshalled into a User domain instance.
Additionally, if possible, I really don't want to have to modify the deserialize implementation if the User domain model changes (e.g. if a field is added or removed over time).
Using Jackson, how do I implement deserialize such that it returns a map of name/value pairs, but converts just the user field into a Java User domain object [...]
One possible way to do it without writing custom deserializers would be:
#Override
public Map<String, Object> deserialize(String json) throws IOException {
Map<String, Object> map =
mapper.readValue(json, new TypeReference<Map<String, Object>>() {});
Optional<Object> optional = Optional.ofNullable(map.get("user"));
if (optional.isPresent()) {
User user = mapper.convertValue(optional.get(), User.class);
map.put("user", user);
}
return map;
}
If the user property is present in the JSON, it's mapped to a User instance and then replaced in the map.

Jackson Custom Deserializer breaks default ones

I'm currently implementing parsing of JSON to POJO.
My Model class looks like this:
public class InvoiceModel {
#JsonDeserialize(using=MeteorDateDeserializer.class)
Date date;
String userId;
String customerName;
ArrayList<PaymentModel> payments;
[...getters, setters...]
}
a JSON string could look like this:
{
"date": {
"$date": 1453812396858
},
"userId": "igxL4tNwR58xuuJbE",
"customerName": "S04",
"payments": [
{
"value": 653.5,
"paymentMethod": "Cash",
"userId": "igxL4tNwR58xuuJbE",
"date": {
"$date": 1453812399033
}
}
]
}
though the payments field may be omitted. Anywho, doesn't really matter too much for the question. You see, the format for parsing the date is somewhat "odd" as in it is encapsulated in the "$date" property of the "date" object. This is however not in my control.
To get around this problem I wrote a custom JSON Deserializer to use for this date property. It looks like this:
public class MeteorDateDeserializer extends org.codehaus.jackson.map.JsonDeserializer<Date> {
#Override
public Date deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JsonProcessingException {
// parse the "$date" field found by traversing the given tokens of jsonParser
while(!jsonParser.isClosed()){
JsonToken jsonToken = jsonParser.nextToken();
if(JsonToken.FIELD_NAME.equals(jsonToken)){
String fieldName = jsonParser.getCurrentName();
jsonToken = jsonParser.nextToken();
if("$date".equals(fieldName)){
long timeStamp = jsonParser.getLongValue();
return new java.util.Date(timeStamp);
}
}
}
return null;
}
}
The exact problem is the following:
The returned InvoiceModel POJO has every attribute apart from "date" set to null, whereas the date is parsed fine. I have narrowed down the problem to the custom Date Deserializer by not deserializing the date at all (just deserializing the 2 string values and the payments array, which works fine).
My thesis is that the annotation conveys to Jackson that the custom deserializer is to be used for the whole class instead of being used just for the date field.
According to the doc this should not be the case:
Annotation use for configuring deserialization aspects, by attaching to "setter" methods or fields, or to
The call to the serialization is nothing special. It's just a standard ObjectMapper call.
InvoiceModel invoice = mapper.readValue(s2, InvoiceModel.class);
where s2 is the JSON string.
My version of Jackson is 1.9.7

Categories

Resources