jackson deserialize object with list of spring's interface - java

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);
}

Related

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...

Using jackson to extract inner JSON array list

My API needs to return a list of entry objects from the JSON below. I am using jersey and jackson. I would ideally like to only create a java class PermissionEnty , and my API to return a list of PermissionEntry objects from the JSON. I am not able to deserialize using the below approach? Can someone advise what could be the issue? I have added UNWRAP_ROOT_VALUE so I presume the 'list' node it ignored, and I would get items below 'list' node.
public class PermissionEntry {
private String id;
private String displayName;
private String memberType;
}
and the json;
{
"list": {
"pagination": {
"count": 5,
"hasMoreItems": false,
},
"entries": [
{
"entry": {
"displayName": "norma",
"id": "norma",
"memberType": "PERSON"
}
},
{
"entry": {
"displayName": "clara",
"id": "clara",
"memberType": "PERSON"
}
},
{
"entry": {
"displayName": "michael",
"id": "mich",
"memberType": "PERSON"
}
}
]
}
}
PermissionEntries
public class PermissionEntries {
#JsonProperty(value = "entries")
#JsonDeserialize(using = PermissionEntryDeserializer.class)
private List<PermissionEntry> entries;
public List<PermissionEntry> getEntries() {
return entries;
}
public void setEntries(List<PermissionEntry> entries) {
this.entries = entries;
}
}
Below is the deserializer that I am using
public class PermissionEntryDeserializer extends JsonDeserializer<List<PermissionEntry>> {
private static final String ENTRY = "entries";
private static final ObjectMapper mapper = new ObjectMapper()
.configure(DeserializationFeature.UNWRAP_ROOT_VALUE, true);
private static final CollectionType collectionType =
TypeFactory
.defaultInstance()
.constructCollectionType(List.class, PermissionEntry.class);
#Override
public List<PermissionEntry> deserialize(JsonParser jsonParser, DeserializationContext deserializationContext)
throws IOException {
ObjectNode objectNode = mapper.readTree(jsonParser);
JsonNode nodeEntries = objectNode.get(ENTRY);
if (null == nodeEntries // if no ENTRY node could be found
|| !nodeEntries.isArray() // or ENTRY node is not an array
|| !nodeEntries.elements().hasNext()) // or ENTRY node doesn't contain any entry
return null;
return mapper.readerFor(collectionType).readValue(nodeEntries);
}
}
Service API
public Optional<List<PermissionEntry>> getPermissionsForGroup(String groupName) {
Response response = getTarget()
.path("/api/group/" + groupName + "/members")
.request()
.get();
PermissionEntries list = response.readEntity(PermissionEntries.class);
}
I don't understand what you mean in this question 'Can someone please tell me how many java classes do I need to create to get a list of entries.' but you had already an entry object called PermissionEntry. You will have the list of this object.
This is the jersey client of your data with jakcson .
ClientConfig clientConfig = new DefaultClientConfig();
clientConfig.getFeatures().put(JSONConfiguration.FEATURE_POJO_MAPPING, Boolean.TRUE);
Client client = Client.create(clientConfig);
String URL = "http://{{host}}:{{port}}/entry";
WebResource webResourceGet = client.resource(URL);
ClientResponse response = webResourceGet.accept("application/json").get(ClientResponse.class);
String output = response.getEntity(String.class);
ResponseList responseList= mapper.readValue(output , ResponseList .class);//here is the permissionEntry list that you wil have
Also, you should create an object given name as Pagination for pagination that is in the json data. You can make an another object that includes List<PermissionEntry> and Pagination called ResponseList.

Jackson Serialization: Different formats for XML and JSON

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.

json into list with Gson

I have a Json like below:
{
"searchResults": {
"searchCriteria": {
"location": {
"originalLocation": null
},
"startAndEndDate": {
"start": "2016-10-06T00:00:00",
"end": "2016-10-09T00:00:00"
},
"solution": [
{
"resultID": "O1MDc1MD",
"selected": false,
"charges": {
"localCurrencyCode": "USD",
"averagePricePerNight": 153
},
"starRating": 3.5
},
{
"resultID": "0MDc1MD",
"selected": false,
"charges": {
"localCurrencyCode": "USD",
"averagePricePerNight": 153
},
"starRating": 3.5
}
....
I have class with attributes starRating and averagePricePerNight which essentially formulates into my POJO.
class ResponseModel {
Int starRating; Int averagePricePerNight
}
I want to parse this JSON and return a List containing :
List(ResponseModel(3.5,900), ResponseModel(3.5,100), ResponseModel(4.5,1000))
I tried to get the json as a List but then i am unable to find examples to get two elements from JSon.
You can write a custom deserializer:
class Deserializers {
public static ResponseModel responseModelDeserializer(JsonElement json, Type typeOfT,
JsonDeserializationContext context) {
JsonObject obj1 = json.getAsJsonObject();
JsonObject obj2 = obj1.get("charges").getAsJsonObject();
double starRating = obj1.get("starRating").getAsDouble();
int averagePricePerNight = obj2.get("averagePricePerNight").getAsInt();
return new ResponseModel(starRating, averagePricePerNight);
}
}
Register it when building Gson:
Gson gson = new GsonBuilder()
.registerTypeAdapter(ResponseModel.class,
(JsonDeserializer<ResponseModel>) Deserializers::responseModelDeserializer
// ^^^ Cast is needed because the parameter has type Object
)
.create();
(Other options include, besides a method reference, are; lambda, anonymous class, or just a regular class. But this one is my favourite.)
Parse your json:
// Get root json object
JsonObject root = new JsonParser().parse(input).getAsJsonObject();
Type tt = new TypeToken<List<ResponseModel>>() {}.getType();
// Get array
List<ResponseModel> mo = gson.fromJson(root.get("solution"), tt);
System.out.println(mo); // [3.5 : 153, 3.5 : 153]
Where ResponseModel is:
class ResponseModel {
private final double starRating;
private final int averagePricePerNight;
public ResponseModel(double starRating, int averagePricePerNight) {
this.starRating = starRating;
this.averagePricePerNight = averagePricePerNight;
}
#Override
public String toString() {
return String.format("%s : %s", starRating, averagePricePerNight);
}
}
I made starRating a double since it seems to be one in your example.

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