Jackson Serialization: Different formats for XML and JSON - java

I use Jackson to serialize/deserialize my application's model into both JSON and XML (need them both).
Model classes:
#JacksonXmlRootElement
public class Data {
#JsonProperty("attributes")
#JsonDeserialize(using = AttributesDeserializer.class)
#JsonSerialize(using = AttributesSerializer.class)
#JacksonXmlElementWrapper
private Map<Key, Map<String, Attribute>> attributes;
....
public class Key {
private Integer id;
private String name;
....
public class Attribute {
private Integer id;
private Integer value;
private String name;
I need my JSON to look like this:
{
"attributes": [
{
"key": {
"id": 10,
"name": "key1"
},
"value": {
"numeric": {
"id": 1,
"value": 100,
"name": "numericAttribute"
},
"text": {
"id": 2,
"value": 200,
"name": "textAttribute"
}
}
},
{
"key": {
"id": 20,
"name": "key2"
},
"value": {
"numeric": {
"id": 1,
"value": 100,
"name": "numericAttribute"
},
"text": {
"id": 2,
"value": 200,
"name": "textAttribute"
}
}
}
]
}
And my XML something like this:
<Data>
<attributes>
<key>
<id>10</id>
<name>key1</name>
</key>
<value>
<numeric>
<id>1</id>
<value>100</value>
<name>numericAttribute</name>
</numeric>
<text>
<id>2</id>
<value>200</value>
<name>textAttribute</name>
</text>
</value>
<key>
<id>20</id>
<name>key2</name>
</key>
<value>
<numeric>
<id>1</id>
<value>100</value>
<name>numericAttribute</name>
</numeric>
<text>
<id>2</id>
<value>200</value>
<name>textAttribute</name>
</text>
</value>
</attributes>
</Data>
I am obtaining both the required JSON and XML with the custom serializer:
public class AttributesSerializer extends JsonSerializer<Map<Key, Map<String, Attribute>>> {
#Override
public void serialize(Map<Key, Map<String, Attribute>> map, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException, JsonProcessingException {
jsonGenerator.writeStartArray();
for (Map.Entry<Key, Map<String, Attribute>> entry : map.entrySet()) {
jsonGenerator.writeStartObject();
jsonGenerator.writeObjectField("key", entry.getKey());
jsonGenerator.writeObjectFieldStart("value");
for (Map.Entry<String, Attribute> attributesEntry : entry.getValue().entrySet()) {
jsonGenerator.writeObjectField(attributesEntry.getKey(), attributesEntry.getValue());
}
jsonGenerator.writeEndObject();
jsonGenerator.writeEndObject();
}
jsonGenerator.writeEndArray();
}
}
And the deserialization works fine for the JSON with the custom deserializer:
public class AttributesDeserializer extends JsonDeserializer<Map<Key, Map<String, Attribute>>> {
#Override
public Map<Key, Map<String, Attribute>> deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JsonProcessingException {
JsonNode node = jsonParser.getCodec().readTree(jsonParser);
if (node.size() == 0) {
return null;
}
ObjectMapper om = new ObjectMapper();
Map<Key, Map<String, Attribute>> attributes = new HashMap<>();
node.forEach(jsonNode -> {
Map<String, Attribute> attributesMap = new HashMap<>();
JsonNode keyNode = jsonNode.get("key");
Key key = om.convertValue(keyNode, Key.class);
JsonNode valueNode = jsonNode.get("value");
Iterator<Map.Entry<String, JsonNode>> attributesIterator = valueNode.fields();
while(attributesIterator.hasNext()) {
Map.Entry<String, JsonNode> field = attributesIterator.next();
Attribute attribute = om.convertValue(field.getValue(), Attribute.class);
attributesMap.put(field.getKey(), attribute);
}
attributes.put(key, attributesMap);
});
return attributes;
}
}
While everything is fine for the JSON, for the XML the application crashes in the deserialization:
Exception in thread "main" com.fasterxml.jackson.databind.JsonMappingException: (was java.lang.NullPointerException) (through reference chain: ro.alexsvecencu.jackson.Data["attributes"])
at com.fasterxml.jackson.databind.JsonMappingException.wrapWithPath(JsonMappingException.java:388)
at com.fasterxml.jackson.databind.JsonMappingException.wrapWithPath(JsonMappingException.java:348)
at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.wrapAndThrow(BeanDeserializerBase.java:1599)
at com.fasterxml.jackson.databind.deser.BeanDeserializer.vanillaDeserialize(BeanDeserializer.java:278)
at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:140)
at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:3798)
at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:2740)
at ro.alexsvecencu.jackson.Main.main(Main.java:27)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:483)
at com.intellij.rt.execution.application.AppMain.main(AppMain.java:134)
Caused by: java.lang.NullPointerException
at ro.alexsvecencu.jackson.AttributesDeserializer.lambda$deserialize$0(AttributesDeserializer.java:29)
at ro.alexsvecencu.jackson.AttributesDeserializer$$Lambda$1/1709366259.accept(Unknown Source)
at java.lang.Iterable.forEach(Iterable.java:75)
at ro.alexsvecencu.jackson.AttributesDeserializer.deserialize(AttributesDeserializer.java:24)
at ro.alexsvecencu.jackson.AttributesDeserializer.deserialize(AttributesDeserializer.java:15)
at com.fasterxml.jackson.databind.deser.SettableBeanProperty.deserialize(SettableBeanProperty.java:499)
at com.fasterxml.jackson.databind.deser.impl.MethodProperty.deserializeAndSet(MethodProperty.java:101)
at com.fasterxml.jackson.databind.deser.BeanDeserializer.vanillaDeserialize(BeanDeserializer.java:276)
... 9 more
What's happening is that my custom deserializer crashes for the XML, because obviously it's not interpreting all the attributes as an 'array' and when I'm going through the jsonNode's children it will iterate through the keys/values. Also, through debugging I notice that the deserializer is just called for the LAST tag of attributes from XML.
Is there any way to tell Jackson to use specific custom deserializers/serializers that are different for XML and JSON? That's one way in which I think this could be solved.
My XML could be formatted a bit different (I'm not really constrained in it's form, but the JSON has to keep that format). With this flexibility, do you see any alternative to solving my issue? I could just use something different for XML, like JAXB, but I'm pretty much constrained to use Jackson for both.

I have a partial solution for you. With Jackson mixin feature, it is possible to have different custom deserializers/serializers for XML and JSON
First, you create another POJO class that has properties with the same name as the ones of Data class, with the different annotations for custom deserializers/serializers
#JacksonXmlRootElement
public static class XmlData
{
#JsonProperty("attributes")
#JsonDeserialize(using = XmlAttributesDeserializer.class) // specify different serializer
#JsonSerialize(using = XmlAttributesSerializer.class) // specify different deserializer
#JacksonXmlElementWrapper
public Map<Key, Map<String, Attribute>> attributes;
}
Next, you create a Jackson Module that associates the Data class with the mixin XmlData class,
#SuppressWarnings("serial")
public static class XmlModule extends SimpleModule
{
public XmlModule()
{
super("XmlModule");
}
#Override
public void setupModule(SetupContext context)
{
context.setMixInAnnotations(Data.class, XmlData.class);
}
}
Here is a test method that shows how to register the module to the mapper and dynamically serialize to different format:
public static void main(String[] args)
{
Attribute a1 = new Attribute();
a1.id = 1;
a1.value = 100;
a1.name = "numericAttribute";
Attribute a2 = new Attribute();
a2.id = 2;
a2.value = 200;
a2.name = "textAttribute";
Map<String, Attribute> atts = new HashMap<>();
atts.put("numeric", a1);
atts.put("text", a2);
Key k1 = new Key();
k1.id = 10;
k1.name = "key1";
Key k2 = new Key();
k2.id = 20;
k2.name = "key2";
Data data = new Data();
data.attributes = new HashMap<>();
data.attributes.put(k1, atts);
data.attributes.put(k2, atts);
ObjectMapper mapper;
if ("xml".equals(args[0])) {
mapper = new XmlMapper();
mapper.registerModule(new XmlModule());
} else {
mapper = new ObjectMapper();
}
try {
mapper.writeValue(System.out, data);
} catch (Exception e) {
e.printStackTrace();
}
}

Besides the solution offered by sharonbn, I've found that if you can encapsulate your field into a different class you can then register a different serializer via a module for either Json (ObjectMapper) or Xml (XmlMapper).
Here's an example in action:
public class CustomSerializers {
public static class PojoField {
PojoField() {
value = "PojoField";
}
public String value;
}
#JacksonXmlRootElement
public static class Pojo {
Pojo() {
field = new PojoField();
}
#JsonProperty("field")
public PojoField field;
}
public static class PojoFieldJSonSerializer extends JsonSerializer<PojoField> {
#Override
public void serialize(PojoField pojoField, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException, JsonProcessingException {
jsonGenerator.writeObject(pojoField.value + " in JSON");
}
}
public static class PojoFieldXmlSerializer extends JsonSerializer<PojoField> {
#Override
public void serialize(PojoField pojoField, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException, JsonProcessingException {
jsonGenerator.writeObject(pojoField.value + " in XMl");
}
}
public static void main(String []args) throws IOException {
Pojo pojo = new Pojo();
SimpleModule objectMapperModule = new SimpleModule();
objectMapperModule.addSerializer(PojoField.class, new PojoFieldJSonSerializer());
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModule(objectMapperModule);
objectMapper.writeValue(new File("pojo.json"), pojo);
SimpleModule xmlMapperModule = new SimpleModule();
xmlMapperModule.addSerializer(PojoField.class, new PojoFieldXmlSerializer());
XmlMapper xmlMapper = new XmlMapper();
xmlMapper.registerModule(xmlMapperModule);
xmlMapper.writeValue(new File("pojo.xml"), pojo);
}
}
The output for JSON will be:
{"field": "PojoField in JSON"}
The output for XML:
<Pojo>
<field>PojoField in XMl</field>
</Pojo>

Could it be the solution to use the only Serializer/Derializer for both Json and Xml, and inside of them check the type of JsonGenerator (json or xml) and apply the logic related to the specific format?
Like that
public class AttributeSerializer extends JsonSerializer<Map<Key, Map<String, Attribute>>> {
#Override
public void serialize(Map<Key, Map<String, Attribute>> value, JsonGenerator gen, SerializerProvider serializers)
throws IOException, JsonProcessingException {
if (gen instanceof ToXmlGenerator) {
//apply logic to format xml structure
} else {
//apply logic to format json or else
}
}
}
I understand that using instaceOf is not a good architecture, but this way we avoid duplication of dto for xml.

Related

How do you convert a Map<Key, Value> into an array of {"keyprop" : key, "valueprop" : value} with Jackson?

I am trying to do the following
Given
Map<String, String> labels = {"en_GB" : "English", "de" : "German", "it" : "Italian"}....
I would like to use Jackson to serialize it to
[{"language" : "en_GB", "label" : "English"}, {"language" : "de", "label" : "German"}, {"language" : "it", "label" : "Italian"}]
Essentially splitting the map into arrays of objects, with the key and value as separate properties
Instead of
{"en_GB" : "English", "de" : "German", "it" : "Italian"}
I have searched the entirety of Jackson docs and i cannot find an answer to this. I would appreciate some help. Thanks in advance!
Method 1: define a middle POJO and convert map<String,String> to list of middle object
first to define a middle POJO:
public class LanguageInfo {
private String language;
private String label;
}
convert map to list of middle POJO and serialize list to string:
Map<String, String> labels = new HashMap<>();
labels.put("en_GB", "English");
labels.put("de", "German");
labels.put("it", "Italian");
List<LanguageInfo> languageInfoList = labels.entrySet().stream()
.map(entry -> {
LanguageInfo info = new LanguageInfo();
info.setLabel(entry.getValue());
info.setLanguage(entry.getKey());
return info;
}).collect(Collectors.toList());
ObjectMapper mapper = new ObjectMapper();
System.out.println(mapper.writeValueAsString(languageInfoList));
output is:
[
{
"language":"de",
"label":"German"
},
{
"language":"en_GB",
"label":"English"
},
{
"language":"it",
"label":"Italian"
}
]
Method 2: define a JsonSerializer and use this to do serialize work
Assume you want to directly serialize the below class:
#Data
public class LanguageInfos {
private Map<String, String> labels;
}
First,define a JsonSerializer to serialize LanguageInfos:
need middle POJO LanguageInfo defined above:
public class LanguageInfosJsonSerializer extends JsonSerializer<LanguageInfos> {
#Override
public void serialize(LanguageInfos languageInfos, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
// LanguageInfo defined in below code
List<LanguageInfo> languageInfoList = languageInfos.getLabels().entrySet().stream()
.map(entry -> {
LanguageInfo info = new LanguageInfo();
info.setLabel(entry.getValue());
info.setLanguage(entry.getKey());
return info;
}).collect(Collectors.toList());
jsonGenerator.writeObject(languageInfoList);
}
}
no need middle POJO:
public class LanguageInfosJsonSerializer extends JsonSerializer<LanguageInfos> {
#Override
public void serialize(LanguageInfos languageInfos, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
jsonGenerator.writeStartArray(languageInfos.getLabels().size());
languageInfos.getLabels().entrySet().forEach(new Consumer<Map.Entry<String, String>>() {
#SneakyThrows
#Override
public void accept(Map.Entry<String, String> entry) {
ObjectNode node = new ObjectMapper().createObjectNode();
node.put("language",entry.getKey());
node.put("label",entry.getValue());
jsonGenerator.writeObject(node);
}
});
jsonGenerator.writeEndArray();
}
}
Second,register this Serializer to a ObjectMapper object:
SimpleModule simpleModule = new SimpleModule();
simpleModule.addSerializer(LanguageInfos.class, new LanguageInfosJsonSerializer());
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(simpleModule);
Third,use the ObjectMapper serialize directly:
Map<String, String> labels = new HashMap<>();
labels.put("en_GB", "English");
labels.put("de", "German");
labels.put("it", "Italian");
LanguageInfos languageInfos = new LanguageInfos();
languageInfos.setLabels(labels);
ObjectMapper mapper = new ObjectMapper();
SimpleModule simpleModule = new SimpleModule();
simpleModule.addSerializer(LanguageInfos.class, new LanguageInfosJsonSerializer());
mapper.registerModule(simpleModule);
System.out.println(mapper.writeValueAsString(languageInfos));
output is:
[
{
"language":"de",
"label":"German"
},
{
"language":"en_GB",
"label":"English"
},
{
"language":"it",
"label":"Italian"
}
]

Jackson - Java Objects with List<Map<String,String>> to Json String conversion

I would like to generate JSON String from Java object
public class Resource {
String name;
List<Item> items;
public String resourceAsJson(Resource resource) throws JsonProcessingException {
ObjectMapper objectMapper = new ObjectMapper();
return objectMapper.writeValueAsString(resource);
}
Where Item
public class Item {
Map<String, String> systemFields;
Map<String, String> dataFields;
}
The form of the JSON String at this moment is
{
"name": "Person",
"items": [
{
"systemFields": {
"systemField1": "xxx",
"systemField2": "xxx",
"systemField3": "x"
},
"dataFields": {
"dataField1": "xxx",
"dataField2": "xxx",
"dataField3": "x"
}
}
]
}
What I try to obtain is the different form of JSON (ommiting the Item and have "system fields" "data fields" in one Json table)
{
"Person":[
{
"systemField1": "xxx",
"systemField2": "xxx",
"systemField3": "Warsaw",
"dataField1": "xxx",
"dataField2": "xxx",
"dataField3": "xxx"
}
]
}
Is there a way to do this with Jackson without changing the model?
In cases like this where default representation of POJO is not what you want you need to implement custom serialisers. In your case they could look like below:
class ResourceJsonSerializer extends JsonSerializer<Resource> {
#Override
public void serialize(Resource value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
gen.writeStartObject();
gen.writeFieldName(value.getName());
gen.writeObject(value.getItems());
gen.writeEndObject();
}
}
class ItemJsonSerializer extends JsonSerializer<Item> {
#Override
public void serialize(Item value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
gen.writeStartObject();
writeMap(value.getSystemFields(), gen);
writeMap(value.getDataFields(), gen);
gen.writeEndObject();
}
private void writeMap(Map<String, String> map, JsonGenerator gen) throws IOException {
if (map != null) {
for (Map.Entry<String, String> entry : map.entrySet()) {
gen.writeStringField(entry.getKey(), entry.getValue());
}
}
}
}
You can register them using com.fasterxml.jackson.databind.annotation.JsonSerialize annotation:
#JsonSerialize(using = ResourceJsonSerializer.class)
class Resource {
and:
#JsonSerialize(using = ItemJsonSerializer.class)
class Item {

Java Jackson, Marshalling class with Map<String, Object> without access to the class code base

I'm trying to marshall (serialize) class using Jackson mapper.
Class has Map property. Property has to have some kind of serialization... everything i got was a serialized byte stream or badly serialized map.toString().
I've tried using mixins or setup Jackson mapper... without any help.
com.fasterxml.jackson 2.8.11
com.rabbitmq.client 5.4.3
My Code:
private RawMessage parseMetadata(RawMessage rawMessage, Envelope envelope, AMQP.BasicProperties properties) throws JsonProcessingException {
ObjectMapper mapper = new ObjectMapper();
mapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);
mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
JsonNode rootNode = mapper.createObjectNode();
JsonNode message = mapper.valueToTree(new String(rawMessage.getPayload()));
((ObjectNode) rootNode).set("message", message);
JsonNode envelopeNode = mapper.valueToTree(envelope);
((ObjectNode) rootNode).set("envelope", envelopeNode);
JsonNode propertiesNode = mapper.valueToTree(properties);
((ObjectNode) rootNode).set("properties", propertiesNode);
return new RawMessage(mapper.writerWithDefaultPrettyPrinter().writeValueAsBytes(rootNode));
}
Result:
{
"properties": {
"bodySize": 0,
"headers": {
"connection_name": {
"bytes": "MTcyLjE5LjAuMTo0NTgzNiAtPiAxNzIuMTkuMC40OjU2NzI=",
"stream": {
"in": { "buf": "MTcyLjE5LjAuMTo0NTgzNiAtPiAxNzIuMTkuMC40OjU2NzI=", "pos": 0, "mark": 0, "count": 35 },
"bytearr": "A...",
"chararr": "\u0000...",
"readBuffer": "AAAAAAAAAAA="
}
},
"timestamp_in_ms": 1565957758662,
"protocol": {
"bytes": "ezAsOSwxfQ==",
"stream": {
"in": { "buf": "ezAsOSwxfQ==", "pos": 0, "mark": 0, "count": 7 },
"bytearr": "AAA...",
"chararr": "\u0000\u0000\...",
"readBuffer": "AAAAAAAAAAA="
}
},...
},
"ssl": false
},
"deliveryMode": 2,
"timestamp": 1565957758000,
"classId": 60,
"className": "basic"
}
}
class BasicProperties extends com.rabbitmq.client.impl.AMQBasicProperties {
private String contentType;
private String contentEncoding;
private Map<String,Object> headers; <---
private Integer deliveryMode;
private Integer priority;
private String correlationId;
private String replyTo;
private String expiration;
private String messageId;
private Date timestamp;
private String type;
private String userId;
private String appId;
private String clusterId;
...}
Reproduction:
import com.rabbitmq.client.AMQP;
Map<String, Object> map = new HashMap<>();
byte[] test = "test".getBytes();
map.put("test", test);
AMQP.BasicProperties prop = new AMQP.BasicProperties(null, null, map,null,null,null,null,null,null,null,null,null,null,null);
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
JsonNode root = objectMapper.createObjectNode();
JsonNode propertiesN = objectMapper.valueToTree(prop);
((ObjectNode) root).set("properties", propertiesN);
LOG.info(root.toString());
Result:
{
"properties": {
"bodySize": 0,
"headers": { "test": "dGVzdA==" },
"classId": 60,
"className": "basic"
}
}
three dots means there's more of it. It's too long. Too many byte streams.
I expect the output of map would be -> key: String
I want to output look like his -> properties.headers.test: "test"
Without any annotation of AMQP.BasicProperties class.
I think that Jacksons Mixins should be the way to do it.
Answer to this question:
- make you own Jackson Mixin that annotates your class with custom JsonSerialize Class
- create you own Serialize class that extends JsonSerializer class
- register Mixin to mapper with addMixIn(DataClass.class, Mixin.class) method
Example:
Create Mixin
abstract class MixIn {
#JsonSerialize(using = MyPairSerializer.class)
abstract Map<String, Object> get_map();
}
Create MapSerializer
class MyPairSerializer extends JsonSerializer<Map<String, Object>> {
public void serialize(Map<String, Object> map, JsonGenerator gen, SerializerProvider serializerProvider) throws IOException {
for (Map.Entry<String, Object> entry : map.entrySet()) {
gen.writeStringField(entry.getKey(), entry.getValue().toString());
}
}
}
Register Serializer and map object
// API call
Data data = getData();
ObjectMapper objectMapper = new ObjectMapper();
JsonNode node = objectMapper.valueToTree(data);
System.out.println("node = " + node);
before:
{"data":"test","map":{"test":"dGVzdA=="}}
after:
{"data":"test","map":{"test":"test"}}
TLDR Answer is Mixins with Custom Serializers...

jackson deserialize object with list of spring's interface

I need to save and load objects from redis.
The object contains list of GrantedAuthority (among other things) which is an interface:
public class UserAccountAuthentication implements Authentication {
private List<GrantedAuthority> authorities;
private boolean authenticated = true;
...
}
Jackson successfully serializes the object but fails to deserialize it with the following exception:
abstract types can only be instantiated with additional type information
I know that I can specify the type by adding:
#JsonTypeInfo(
But I can't do it in this case because the GrantedAuthority is an interface of Spring and I cannot change it.
the serialized json is:
{
"authorities": [
{
"authority": "ROLE_NORMAL_USER"
}
],
"authenticated": true,
"securityToken": {
"expiration": 1458635906853,
"token": "sxKi3Pddfewl2rgpatVE7KiSR5qGmhpGl0spiHUTLAAW8zuoLFE0VLFYcfk72VLnli66fcVmb8aK9qFavyix3bOwgp1DRGtGacPI",
"roles": [
"ROLE_NORMAL_USER"
],
"expired": false,
"expirationDateFormatted": "2016-03-22 08:38:26.853 UTC"
},
"name": "admin",
"expired": false
}
the abstract GrantedAuthority is only filled with SimpleGrantedAuthority.
so i tried:
objectMapper.registerSubtypes(SimpleGrantedAuthority.class);
and still no luck.
I think you need to add a custom deserializer
public class UserAccountAuthenticationSerializer extends JsonDeserializer<UserAccountAuthentication> {
#Override
public UserAccountAuthentication deserialize(JsonParser jsonParser, DeserializationContext deserializationContext)
throws IOException {
UserAccountAuthentication userAccountAuthentication = new UserAccountAuthentication();
ObjectCodec oc = jsonParser.getCodec();
JsonNode node = oc.readTree(jsonParser);
userAccountAuthentication.setAuthenticated(node.get("authenticated").booleanValue());
Iterator<JsonNode> elements = node.get("authorities").elements();
while (elements.hasNext()) {
JsonNode next = elements.next();
JsonNode authority = next.get("authority");
userAccountAuthentication.getAuthorities().add(new SimpleGrantedAuthority(authority.asText()));
}
return userAccountAuthentication;
}
}
This is my json
{"authenticated":true,"authorities":[{"authority":"role1"},{"authority":"role2"}],"details":null,"principal":null,"credentials":null,"name":null}
Then at the top of your POJO
#JsonDeserialize(using = UserAccountAuthenticationSerializer.class)
public class UserAccountAuthentication implements Authentication {
Here's the test
#Test
public void test1() throws IOException {
UserAccountAuthentication userAccountAuthentication = new UserAccountAuthentication();
userAccountAuthentication.setAuthenticated(true);
userAccountAuthentication.getAuthorities().add(new SimpleGrantedAuthority("role1"));
userAccountAuthentication.getAuthorities().add(new SimpleGrantedAuthority("role2"));
String json1 = new ObjectMapper().writeValueAsString(userAccountAuthentication);
UserAccountAuthentication readValue = new ObjectMapper().readValue(json1, UserAccountAuthentication.class);
String json2 = new ObjectMapper().writeValueAsString(readValue);
assertEquals(json1, json2);
}

How do I parse JSON into a Map with lowercase keys using Jackson?

I am using the Jackson (1.9.x) library to parse JSON into a Map:
ObjectMapper mapper = new ObjectMapper();
Map<String,Object> map = (Map<String,Object>) mapper.readValue(jsonStr, Map.class);
Is there a way to tell the Jackson parser to lowercase all the names of the keys? I tried using a Jackson PropertyNamingStrategy, but that didn't work - it only seems to be useful when it is getting mapped onto some bean, not a Map.
Clarifications:
I do not want to have to precreate beans for the JSON - I only want dynamic Maps
The JSON keys coming in will not be lowercase, but I want all the map keys to be lowercase (see example below)
The JSON is rather large and heavily nested, so regular expression replacements of the incoming JSON or creating a new map manually after the Jackson parsing is not at all desired.
Incoming JSON:
{"CustName":"Jimmy Smith","Result":"foo","CustNo":"1234"}
The Java map would have:
"custname" => "Jimmy Smith"
"result" => "foo"
"custno" => "1234"
[UPDATE]: The answer I gave below doesn't fully solve the problem. Still looking for a solution.
(nb this solution is tested only with Jackson 2)
It's possible to do this by wrapping the JsonParser and simply applying .toLowerCase() to all field names:
private static final class DowncasingParser extends JsonParserDelegate {
private DowncasingParser(JsonParser d) {
super(d);
}
#Override
public String getCurrentName() throws IOException, JsonParseException {
if (hasTokenId(JsonTokenId.ID_FIELD_NAME)) {
return delegate.getCurrentName().toLowerCase();
}
return delegate.getCurrentName();
}
#Override
public String getText() throws IOException, JsonParseException {
if (hasTokenId(JsonTokenId.ID_FIELD_NAME)) {
return delegate.getText().toLowerCase();
}
return delegate.getText();
}
}
You then have to have a custom JsonFactory to apply your wrapper, as in this test:
#Test
public void downcase_map_keys_by_extending_stream_parser() throws Exception {
#SuppressWarnings("serial")
ObjectMapper mapper = new ObjectMapper(new JsonFactory() {
#Override
protected JsonParser _createParser(byte[] data, int offset, int len, IOContext ctxt) throws IOException {
return new DowncasingParser(super._createParser(data, offset, len, ctxt));
}
#Override
protected JsonParser _createParser(InputStream in, IOContext ctxt) throws IOException {
return new DowncasingParser(super._createParser(in, ctxt));
}
#Override
protected JsonParser _createParser(Reader r, IOContext ctxt) throws IOException {
return new DowncasingParser(super._createParser(r, ctxt));
}
#Override
protected JsonParser _createParser(char[] data, int offset, int len, IOContext ctxt, boolean recyclable)
throws IOException {
return new DowncasingParser(super._createParser(data, offset, len, ctxt, recyclable));
}
});
assertThat(
mapper.reader(Map.class)
.with(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES)
.with(JsonParser.Feature.ALLOW_SINGLE_QUOTES)
.readValue("{CustName:'Jimmy Smith', CustNo:'1234', Details:{PhoneNumber:'555-5555',Result:'foo'} } }"),
equalTo((Map<String, ?>) ImmutableMap.of(
"custname", "Jimmy Smith",
"custno", "1234",
"details", ImmutableMap.of(
"phonenumber", "555-5555",
"result", "foo"
)
)));
}
I figured out one way to do it. Use a org.codehaus.jackson.map.KeyDeserializer, put it in a SimpleModule and register that module with the Jackson ObjectMapper.
import org.codehaus.jackson.map.KeyDeserializer;
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.map.module.SimpleModule;
import org.codehaus.jackson.Version;
// ...
class LowerCaseKeyDeserializer extends KeyDeserializer {
#Override
public Object deserializeKey(String key, DeserializationContext ctx)
throws IOException, JsonProcessingException {
return key.toLowerCase();
}
}
// ...
ObjectMapper mapper = new ObjectMapper();
SimpleModule module = new SimpleModule("LowerCaseKeyDeserializer",
new Version(1,0,0,null));
module.addKeyDeserializer(Object.class, new LowerCaseKeyDeserializer());
mapper.registerModule(module);
Map<String,Object> map =
(Map<String,Object>) mapper.readValue(jsonStr, Map.class);
[UPDATE]: Actually this only will lowercase the top level map keys, but not nested keys.
If the input is:
{"CustName":"Jimmy Smith","CustNo":"1234","Details":{"PhoneNumber": "555-5555", "Result": "foo"}}
The output in the map, unfortunately, will be:
{"custname"="Jimmy Smith", "custno"="1234", "details"={"PhoneNumber"="555-5555", "Result"="foo"}}
With Jackson there isn't any function that will lower the keys in a nested fashion. Atleast not that I know of. I wrote this simple recursive code that does the job.
public JSONObject recursiveJsonKeyConverterToLower(JSONObject jsonObject) throws JSONException
{
JSONObject resultJsonObject = new JSONObject();
#SuppressWarnings("unchecked") Iterator<String> keys = jsonObject.keys();
while(keys.hasNext())
{
String key = keys.next();
Object value = null;
try
{
JSONObject nestedJsonObject = jsonObject.getJSONObject(key);
value = this.recursiveJsonKeyConverterToLower(nestedJsonObject);
}
catch(JSONException jsonException)
{
value = jsonObject.get(key);
}
resultJsonObject.put(key.toLowerCase(), value);
}
return resultJsonObject;
}
Passed String:
String json = "{'Music': 0, 'Books': {'Biology': 1.1, 'Chemistry': {'Inorganic': true, 'Organic': ['Atom', 'Molecule']}}, 'Food': {'Chicken': [1, 2, 3]}}";
Output:
{"music":0,"books":{"biology":1.1,"chemistry":{"inorganic":true,"organic":["Atom","Molecule"]}},"food":{"chicken":[1,2,3]}}
Its also easy to get Map<String, Object> instead of JSONObject (which is what you want) by making resultJsonObject to be of type Map and other little tweaks.
WARNING: for nested JSON, the result would be of type Map<String, Map<String, Object>> depending on how nested is your json object.
public void setKeyName(String systemName){
this.systemName = systemName.toLowerCase();
}
Below is the second JSON message:
{
"ModeL":"Tesla",
"YeaR":"2015"
}
Normally, default ObjectMapper cannot deserialize this message into a CarInfo object. With following configuration, it’s possible:
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES, true);
CarInfo info = objectMapper.readValue(data, CarInfo.class); //'data' contains JSON string
This deserialization is valid. his deserialization is valid.
https://mtyurt.net/post/jackson-case-insensitive-deserialization.html

Categories