Jackson deserialise JSON with many dynamic nodes - java

I'm trying to deserialize a JSON file with the format of
{
"English": {
"hex": "FF0000"
},
"Spanish": {
"hex": "0000FF"
},
"Japanese": {
"hex": "FFFF00"
}
}
But I don't want to create a class for each language (thousands) so I wrote a custom LanguageDeserializer which gives me back the List<Language> that I want
static class LanguageDeserializer extends StdDeserializer<ArrayList<Language>> {
//...
#Override
public ArrayList<Language> deserialize(final JsonParser jp, final DeserializationContext ctxt) throws IOException {
final JsonNode node = jp.getCodec().readTree(jp);
final Iterator<Map.Entry<String, JsonNode>> nodes = node.fields();
final ArrayList<Language> results = new ArrayList<>();
while (nodes.hasNext()) {
// Builds up the object from the nodes
results.add(builder.build());
}
return results;
}
I have a parent class to wrap the results:
public class LanguageWrapper {
#JsonDeserialize(using = Language.LanguageDeserializer.class)
public List<Language> languages = new ArrayList<>();
}
So when I try and use it
final LanguageWrapper languageWrapper = objectMapper.readValue(new ClassPathResource("data/languages.json").getFile(), LanguageWrapper.class);
The languages list is always empty.
Can I do this without needing the LanguageDeserializer or how do I make it work?

Yeah was clearly overthinking it. As #chrylis suggested a Map was the right direction to go in.
Simple as:
final TypeReference<HashMap<String, Language>> typeRef = new TypeReference<>() {};
final HashMap<String, Language> hashMap = objectMapper.readValue(new ClassPathResource("data/languages.json").getFile(), typeRef);

If list (and plain Language class) suits you better, you can do:
TypeReference<Map<String, Map<String, String>>> typeReference = new TypeReference<>() {};
List<Language> languages = new ObjectMapper().readValue(new ClassPathResource("data/languages.json").getFile(), typeReference)
.entrySet()
.stream()
.map(entry -> new Language(entry.getKey(), entry.getValue().get("hex")))
.collect(Collectors.toList());

Related

Guava Hashmap comparison

I'm having some trouble using Guava's Maps.difference
Right now, using this code to compare two HashMaps from two different jsons:
//Create maps from the given jsons
Gson gson = new Gson();
Type type = new TypeToken<Map<String, Object>>(){}.getType();
Map<String, Object> map1 = gson.fromJson(jsonObject1, type);
Map<String, Object> map2 = gson.fromJson(jsonObject2, type);
//Flatten the maps
Map<String, Object> leftFlatMap = FlatMap.flatten(map1);
Map<String, Object> rightFlatMap = FlatMap.flatten(map2);
//Check differences between both maps
MapDifference<String, Object> difference = Maps.difference(leftFlatMap, rightFlatMap);
Everything works fine, and compares (almost) all the elements correctly.
Problem is when one of the elements inside the HashMap is an array of maps and the elements are the same but in a different order. Like this:
FIRST JSON:
{ "body":[
{
"primitive":"VALUE",
"jsonArray":[
{
"element":83284180
},
{
"anotherElement":20832841804
}
]
}
]
}
SECOND JSON:
{
"body":[
{
"primitive":"VALUE",
"jsonArray":[
{
"anotherElement":20832841804
},
{
"element":83284180
}
]
}
]
}
As you can see, element and anotherElement values are the same but as they appear in a different order inside the array, difference shows an error.
Is there any possibility to sort the array before? or any other solution?
Thanks in advance!!
One of possible solutions may be sorting the inner sub array so that it would affect the deserialized maps (however, I think making maps out of JSON objects in this case might be a not very good idea due to deserialization costs and strategies that do not necessarily represent the original JSON object).
Assuming jsonObject1 and jsonObject2 are JsonElement implementations, you can sort its descendants.
#UtilityClass
public final class JsonElements {
public static List<JsonElement> asListView(final JsonArray jsonArray) {
return new JsonArrayListView(jsonArray);
}
public static void sort(final JsonArray jsonArray, final Comparator<? super JsonElement> comparator) {
Collections.sort(asListView(jsonArray), comparator);
}
#AllArgsConstructor(access = AccessLevel.PRIVATE)
private static final class JsonArrayListView
extends AbstractList<JsonElement> {
private final JsonArray jsonArray;
#Override
public JsonElement get(final int index) {
return jsonArray.get(index);
}
#Override
public int size() {
return jsonArray.size();
}
#Override
#SuppressWarnings("MethodDoesntCallSuperMethod")
public JsonElement set(final int index, final JsonElement element) {
return jsonArray.set(index, element);
}
}
}
public final class JsonElementsTest {
private static final Gson gson = new GsonBuilder()
.disableHtmlEscaping()
.disableInnerClassSerialization()
.create();
private static final Type stringToObjectMapType = new TypeToken<Map<String, Object>>() {}.getType();
#Test
public void testSort()
throws IOException {
final JsonElement jsonElement1 = ... read the 1st JSON document ...;
final JsonElement jsonElement2 = ... read the 2nd JSON document ...;
final JsonArray jsonSubArray1 = getSubArray(jsonElement1);
final JsonArray jsonSubArray2 = getSubArray(jsonElement2);
Assertions.assertNotEquals(jsonSubArray1, jsonSubArray2);
JsonElements.sort(jsonSubArray1, JsonElementsTest::compare);
JsonElements.sort(jsonSubArray2, JsonElementsTest::compare);
final Map<String, Object> map1 = gson.fromJson(jsonElement1, stringToObjectMapType);
final Map<String, Object> map2 = gson.fromJson(jsonElement2, stringToObjectMapType);
Assertions.assertEquals(map1, map2);
}
private static JsonArray getSubArray(final JsonElement jsonElement) {
return jsonElement.getAsJsonObject()
.get("body")
.getAsJsonArray()
.get(0)
.getAsJsonObject()
.get("jsonArray")
.getAsJsonArray();
}
private static int compare(final JsonElement jsonElement1, final JsonElement jsonElement2)
throws IllegalArgumentException {
final JsonObject jsonObject1 = jsonElement1.getAsJsonObject();
final int size1 = jsonObject1.size();
if ( size1 != 1 ) {
throw new IllegalArgumentException("Size-1 must equal 1, but was " + size1);
}
final JsonObject jsonObject2 = jsonElement2.getAsJsonObject();
final int size2 = jsonObject2.size();
if ( size2 != 1 ) {
throw new IllegalArgumentException("Size-2 must equal 2, but was " + size2);
}
// TODO optimize somehow
final String key1 = jsonObject1.keySet().iterator().next();
final String key2 = jsonObject2.keySet().iterator().next();
return key1.compareTo(key2);
}
}
Also consider sorting the descendants recursively if necessary.
Note that you also might have a mapping for the given JSON documents, but I don't think it's your case, but if it is, then you might want to apply #JsonAdapter to apply a special ordering deserializer (however I still don't think it's a good idea too). Or else, it is also possible to create a map view for the given JsonObjects so that it might produce recursive reordering views.

Custom deserializer for TreeMap - cannot get full text by JsonParser

I have a class defined as following:
class Rule {
private final TreeMap<String, String> rule;
public Rule(TreeMap<String, String> rule) {
this.rule = rule;
}
}
Another class contains a list of such object:
class BigClass {
#JsonProperty("rules")
#JsonDeserialize(contentUsing = Rule.Deserializer.class)
private final List<Rule> rules;
//...
}
... and as specified in the #JsonDeserialize annotation, I want the content of such list to be treated by a custom deserializer.
For that, I have extended the JsonDeserializer<Rule> as follows:
public static final class Deserializer extends JsonDeserializer<Rule> {
private final ObjectMapper objectMapper = new ObjectMapper();
#Override
public Rule deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
String strRule = p.getText();
TypeReference<TreeMap<String, String>> typeReference = new TypeReference<>() {
};
TreeMap<String, String> ruleMap = objectMapper.readValue(strRule, typeReference);
return new Rule(ruleMap);
}
}
When I try to parse a Json of type BigClass with the following structure:
{
...
"rules": [
{
"A": "B",
"C": "D",
"E": "F"
}
]
}
... I am correctly called inside my own deserialize() method, but the problem is that the expression p.getText() only returns me { (the very first token), so the deserialization fails.
I have tried to put everything in the same line (no carriage return), but I have the same problem.
It seems something obvious and simple but I can't find any example on the web nor in the Jackson documentation.
Any suggestion or reference to share please?
Change your deserialize method as follows:
You read the whole JsonNode in the deserialization step, and construct your own TreeMap based on the key-values that you find in the node. (you may have to add a constructor to the Rule class)
#Override
public Rule deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
JsonNode node = p.getCodec().readTree(p);
TreeMap<String, String> ruleMap = new TreeMap<>();
node.fields().forEachRemaining(e -> ruleMap.put(e.getKey(), e.getValue().textValue()));
return new Rule(ruleMap);
}

Supply Long Object Pool to Jackson Object Mapper

I have a JSON that I convert into POJOs. The JSON is read from a GZIPInputStream gis.
ObjectMapper mapper = new ObjectMapper();
TypeReference<Map<Long, ConfigMasterAirportData>> typeRef =
new TypeReference<Map<Long, ConfigMasterAirportData>>() {};
Map<Long, ConfigMasterAirportData> configMasterAirportMap =
mapper.readValue(gis, typeRef);
I do not want new Long objects to be created for each entry. I want it to get Long objects from a custom LongPool I have created. Is there a way to pass such a LongPool to the mapper?
If not, is there another JSON library I can use to do that?
There are many ways to achieve this if you are sure that object pooling is required in your case.
First of all, Java already does Long object pooling for a small range between -128 and 127 inclusive. See source code of Long.valueOf.
Let us have 2 JSON objects that we want to deserialize: map1 and map2:
final String map1 = "{\"1\": \"Hello\", \"10000000\": \"world!\"}";
final String map2 = "{\"1\": \"You\", \"10000000\": \"rock!\"}";
Standard deserialization
If we use standard deserialization:
final ObjectMapper mapper = new ObjectMapper();
final TypeReference<Map<Long, String>> typeRef = new TypeReference<Map<Long, String>>() {};
final Map<Long, String> deserializedMap1 = mapper.readValue(map1, typeRef);
final Map<Long, String> deserializedMap2 = mapper.readValue(map2, typeRef);
printMap(deserializedMap1);
printMap(deserializedMap2);
Where printMap is defined as
private static void printMap(Map<Long, String> longStringMap) {
longStringMap.forEach((Long k, String v) -> {
System.out.printf("key object id %d \t %s -> %s %n", System.identityHashCode(k), k, v);
});
}
we get the following output:
key object id 1635756693 1 -> Hello
key object id 504527234 10000000 -> world!
key object id 1635756693 1 -> You
key object id 101478235 10000000 -> rock!
Note that key 1 is the same object with hashcode 1635756693 in both maps. This is due to built-in pool for [-128,127] range.
Solution1: #JsonAnySetter deserialization
We can define a wrapper object for the map and use #JsonAnySetter annotation to intercept all key-value pairs being deserialized. Then we can intern each Long object using Guava StrongInterner:
static class CustomLongPoolingMap {
private static final Interner<Long> LONG_POOL = Interners.newStrongInterner();
private final Map<Long, String> map = new HashMap<>();
#JsonAnySetter
public void addEntry(String key, String value) {
map.put(LONG_POOL.intern(Long.parseLong(key)), value);
}
public Map<Long, String> getMap() {
return map;
}
}
We will use it like this:
final ObjectMapper mapper = new ObjectMapper();
final Map<Long, String> deserializedMap1 = mapper.readValue(map1, CustomLongPoolingMap.class).getMap();
final Map<Long, String> deserializedMap2 = mapper.readValue(map2, CustomLongPoolingMap.class).getMap();
Output:
key object id 1635756693 1 -> Hello
key object id 1596467899 10000000 -> world!
key object id 1635756693 1 -> You
key object id 1596467899 10000000 -> rock!
Now you can see that key 10000000 is also the same object in both maps with hashcode 1596467899
Solution 2: Register custom KeyDeserializer
Define custom KeySerializer:
public static class MyCustomKeyDeserializer extends KeyDeserializer {
private static final Interner<Long> LONG_POOL = Interners.newStrongInterner();
#Override
public Long deserializeKey(String key, DeserializationContext ctxt) {
return LONG_POOL.intern(Long.parseLong(key));
}
}
And register it with the ObjectMapper:
final SimpleModule module = new SimpleModule();
module.addKeyDeserializer(Long.class, new MyCustomKeyDeserializer());
final ObjectMapper mapper = new ObjectMapper().registerModule(module);
final TypeReference<Map<Long, String>> typeRef = new TypeReference<Map<Long, String>>() {};
final Map<Long, String> deserializedMap1 = mapper.readValue(map1, typeRef);
final Map<Long, String> deserializedMap2 = mapper.readValue(map2, typeRef);
Solution 3: Use custom KeyDeserializer via #JsonDeserialize annotation
Define a wrapper object
static class MapWrapper {
#JsonDeserialize(keyUsing = MyCustomKeyDeserializer.class)
private Map<Long, String> map1;
#JsonDeserialize(keyUsing = MyCustomKeyDeserializer.class)
private Map<Long, String> map2;
}
And deserialize it:
final ObjectMapper mapper = new ObjectMapper();
final String json = "{\"map1\": " + map1 + ", \"map2\": " + map2 + "}";
final MapWrapper wrapper = mapper.readValue(json, MapWrapper.class);
final Map<Long, String> deserializedMap1 = wrapper.map1;
final Map<Long, String> deserializedMap2 = wrapper.map2;
Solution 4: Use Trove library TLongObjectMap to avoid using Long objects entirely
Trove library implements maps that use primitive types for keys to remove overhead of boxed objects entirely. It's in somewhat dormant state however.
You need in your case TLongObjectHashMap.
There is a library that defines a deserializer for TIntObjectMap:
https://bitbucket.org/marshallpierce/jackson-datatype-trove/src/d7386afab0eece6f34a0af69b76b478f417f0bd4/src/main/java/com/palominolabs/jackson/datatype/trove/deser/TIntObjectMapDeserializer.java?at=master&fileviewer=file-view-default
I think it will be quite easy to adapt it for TLongObjectMap.
Full code for this answer can be found here: https://gist.github.com/shtratos/f0a81515d19b858dafb71e86b62cb474
I've used answers to this question for solutions 2 & 3:
Deserializing non-string map keys with Jackson
Not sure about Jackson library, but with Google Gson you can quite simply do it by registering a custom type adapter whose responsibility is to resolve every key the way you want it:
public class DeserializeJsonMapWithCustomKeyResolver {
public static void main(String[] args) {
final String JSON = "{ \"1\" : { \"value\" :1 }, \"2\" : { \"value\" : 2} }";
final Type mapType = new TypeToken<Map<Long, ConfigMasterAirportData>>() {}.getType();
final Map<String, ConfigMasterAirportData> map =
new GsonBuilder().registerTypeAdapter(mapToken, new PooledLongKeyDeserializer())
.create()
.fromJson(JSON, mapType);
System.out.println(map);
}
static Long longFromString(String value)
{
System.out.println("Resolving value : " + value);
// TODO: replace with your LongPool call here instead; may need to convert from String
return Long.valueOf(value);
}
static class PooledLongKeyDeserializer implements
JsonDeserializer<Map<Long, ConfigMasterAirportData>>
{
#Override
public Map<Long, ConfigMasterAirportData> deserialize(
JsonElement json,
Type typeOfT,
JsonDeserializationContext context)
throws JsonParseException
{
final Map<Long, ConfigMasterAirportData> map = json.getAsJsonObject()
.entrySet()
.stream()
.collect(
Collectors.toMap(
e -> longFromString(e.getKey()),
e -> context.deserialize(e.getValue(),
TypeToken.get(ConfigMasterAirportData.class).getType())
));
return map;
}
}
static class ConfigMasterAirportData {
public int value;
#Override
public String toString() { return "ConfigMasterAirportData{value=" + value + '}'; }
}
}

How to convert List<Entity> to List<DTO> Objects using object mapper?

I have a method like this:
public List<CustomerDTO> getAllCustomers() {
Iterable<Customer> customer = customerRepository.findAll();
ObjectMapper mapper = new ObjectMapper();
return (List<CustomerDTO>) mapper.convertValue(customer, CustomerDTO.class);
}
when i try to convert List values i am getting below message
com.fasterxml.jackson.databind.JsonMappingException: Can not deserialize instance of com.finman.customer.CustomerDTO out of START_ARRAY token
mapper.convertValue(customer, CustomerDTO.class)
This attempts to create a single CustomerDTO, not a list of them.
Perhaps this will help:
mapper.readValues(customer, CustomerDTO.class).readAll()
You can do something like:
static <T> List<T> typeCastList(final Iterable<?> fromList,
final Class<T> instanceClass) {
final List<T> list = new ArrayList<>();
final ObjectMapper mapper = new ObjectMapper();
for (final Object item : fromList) {
final T entry = item instanceof List<?> ? instanceClass.cast(item) : mapper.convertValue(item, instanceClass);
list.add(entry);
}
return list;
}
// And the usage is
final List<DTO> castedList = typeCastList(entityList, DTO.class);

ObjectMapper readValue

I load a ressource file json
with the text format
{
"sources": [{
"prop1": "1",
"prop2": "2"
},
{
"prop1": "1",
"prop2": "2"
},
],
"redirection": [{
"prop1": "1",
"prop2": "2"
}
]
}
I have a class with this properties prop1 and prop2
I want to recover with ObjectMapper a list class. What the method ?
This code doesn't work ....
Map<String, Object> mp = mapper.readValue(jsonResource.getInputStream(),new TypeReference<Map<String, Object>>() {});
String sourceText= new ObjectMapper().readTree(jsonResource.getInputStream()).get("sources").asText();
mapper.readValue(sourceText, new TypeReference<List<MyClass>>(){});
Thanks for your help
In your case, I would write a custom JsonDeserializer. Haven't really tested the code, but I think the idea is clear:
final MyClassDeserializer myClassDeserializer = new MyClassDeserializer();
final SimpleModule deserializerModule = new SimpleModule();
deserializerModule.addDeserializer(MyClass.class, myClassDeserializer);
final ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(deserializerModule);
And the code for JsonDeserializer:
public class MyClassDeserializer extends JsonDeserializer<MyClass> {
#Override
public MyClass deserialize(final JsonParser jsonParser, final DeserializationContext context)
throws IOException {
final JsonNode node = jsonParser.getCodec().readTree(jsonParser);
final JsonNode sourcesNode = node.get("sources");
if(node.isArray()) {
final ArrayNode arrayNode = (ArrayNode) node;
final Iterable<JsonNode> nodes = arrayNode::elements;
final Set<Source> set = StreamSupport.stream(nodes.spliterator(), false)
.map(mapper)
.collect(Collectors.toSet());
...
}
...
}
First thing: Your JSON is invalid. There is a comma after the second object in the sources array. This has to be deleted.
Second: I think you didn't choose the right type for your result. What your JSON represents is a map which maps from string to an array of objects. So the type should be something like Map<String, Props[]> (Since you didn't provide the name of your class, I called it Props.
With these considerations you can construct a MapType by using ObjectMappers getTypeFactory() method and deserialize the value using the constructed type like shown below.
ObjectMapper mapper = new ObjectMapper();
TypeFactory typeFactory = mapper.getTypeFactory();
MapType mapType = typeFactory.constructMapType(HashMap.class, String.class, Props[].class);
Map<String, Props[]> map = mapper.readValue(s, mapType);
I actually voted for the other answer, but this is my idea, to create the classes and let jackson do the work :
public class ResourceTest {
#Test
public void test1() throws IOException {
assertTrue(true);
Resource resource = new Resource();
resource.getRedirectrions().add(makeRedirectrion("rprop11", "rprop12"));
resource.getRedirectrions().add(makeRedirectrion("rprop21", "rprop22"));
resource.getSources().add(makeSource("sprop11","sprop12"));
resource.getSources().add(makeSource("sprop21","sprop22"));
String json = new ObjectMapper().writeValueAsString(resource);
System.out.println(json);
Resource resource1 = new ObjectMapper().readValue(json, Resource.class);
System.out.println(resource1);
}
private Source makeSource(String prop1, String prop2) {
Source source = new Source();
source.setProp1(prop1);
source.setProp2(prop2);
return source;
}
private Redirectrion makeRedirectrion(String prop1, String prop2) {
Redirectrion redirectrion = new Redirectrion();
redirectrion.setProp1(prop1);
redirectrion.setProp2(prop2);
return redirectrion;
}
}
Output is:
{"sources":[{"prop1":"sprop11","prop2":"sprop12"},{"prop1":"sprop21","prop2":"sprop22"}],"redirectrions":[{"prop1":"rprop11","prop2":"rprop12"},{"prop1":"rprop21","prop2":"rprop22"}]}
Resource{sources=[Source{prop1='sprop11', prop2='sprop12'}, Source{prop1='sprop21', prop2='sprop22'}], redirectrions=[Source{prop1='rprop11', prop2='rprop12'}, Source{prop1='rprop21', prop2='rprop22'}]}

Categories

Resources