to sum it up before the wall of text below :-)
I need help with how to deserialize a Dictionary using Jackson and a custom deserializer.
Right now I have an Android app communication with a .NET (C#) server. They use JSON to communicate.
On the JAVA-side, I am using Jackson to handle the JSON and on the .NET-side I am using the built in DataContractSerializer (I know, ppl will start commenting I should use something else, but Im not so... ;-) )
The problem is that I am sending Dictionaries from C# and I want that to be deserialized to HashMaps om the JAVA-side, but I havent found a good resource for how to do that.
One example of a Dictionary I am sending from C#:
// Here, the object Equipment is the key, and the int following indicates the amount
[DataMember]
public Dictionary<Equipment, int> EquipmentList { get; set; }
And just for reference, the Equipment object in C#:
[DataContract]
public class Equipment
{
[DataMember]
public uint Id { get; set; }
[DataMember]
public string Name { get; set; }
public override bool Equals(object obj)
{
if (obj.GetType() != this.GetType())
return false;
Equipment e = (Equipment)obj;
return e.Id == this.Id;
}
}
Its serialized correctly into decent JSON on the C#-side, the Dictionary looks like this:
//....
"EquipmentList":[
{
"Key":{
"EquipmentId":123,
"Name":"MyName"
},
"Value":1
}
//....
I have added a custom serializer (CustomMapSerializer), like this:
public static ObjectMapper mapper = new ObjectMapper();
private static SimpleDeserializers simpleDeserializers = new SimpleDeserializers();
private static StdDeserializerProvider sp = new StdDeserializerProvider();
public static void InitSerialization()
{
simpleDeserializers.addDeserializer(String.class, new CustomStringDeserializer());
simpleDeserializers.addDeserializer(Map.class, new CustomMapDeserializer());
sp.withAdditionalDeserializers(simpleDeserializers);
mapper.setDeserializerProvider(sp);
}
And decorated the field like this:
#JsonDeserialize(using=CustomMapDeserializer.class)
public Map<Equipment, Integer> EquipmentList;
And finally, when I run it I do get a break in the custom deserializer class, but I am not sure how to proceed from here:
public class CustomMapDeserializer extends JsonDeserializer<Map> {
#Override
public Map deserialize(JsonParser arg0, DeserializationContext arg1) throws IOException, JsonProcessingException
{
return new HashMap<Object, Object>(); // <-- I can break here
}
}
So, what I would like is some input on how to create a HashMap with the correct values in it, ie a deserialized Equipment as Key and an Int as value.
Anyone out there who can assist? =)
Ok, after a while testing and researching, this is what I came up with.
The custom deserializer looks like this:
public class CustomMapCoTravellerDeserializer extends JsonDeserializer<Map> {
#Override
public Map deserialize(JsonParser jp, DeserializationContext arg1) throws IOException, JsonProcessingException
{
HashMap<CoTravellers, Integer> myMap = new HashMap<CoTravellers, Integer>();
CoTravellers ct = new CoTravellers();
jp.nextToken(); // {
jp.nextToken(); // Key
jp.nextToken(); // {
jp.nextToken(); // "CoTravellerId"
jp.nextToken(); // CoTravellerId Id
int coTravellerValue = jp.getIntValue();
jp.nextToken(); // Name
jp.nextToken(); // Name Value
String coTravellerName = jp.getText();
jp.nextToken(); // }
jp.nextToken(); // "Value"
jp.nextToken(); // The value
int nbr = jp.getIntValue();
ct.CoTravellerId = coTravellerValue;
ct.Name = coTravellerName;
myMap.put(ct, nbr);
return myMap;
}
}
I think this will work, if I can only figure out the JsonMappingException I am getting... but I will post on that separately =)
Related
I have two apis which is return following json responses.
I created a class called 'Card' and how should I achieve 'expiry' field which have different type for particular request.
Thanks in advance.
For simplicity i'll work only with card object from example, i won't be wrapping it another object. To avoid duplication in examples, i'll use this base class to hold common fields:
public abstract class BaseCard {
private String brand;
private String fundingMethod;
private String scheme;
//setters and getters
}
You have few options.
Option 1:
If you know which api you are getting response from, you can have two classes, each holding expiry in format specific for the api. When getting data from api 1 use:
public class CardApi1 extends BaseCard {
private String expiry;
//setters and getters
}
And for api 2:
public class CardApi2 extends BaseCard {
private Expiry expiry;
//setters and getters
}
The expiry object looks like this:
public class Expiry {
private String month;
private String year;
//setters and getters
}
If you are calling api 1, deserialize to CardApi1, else to CardApi2.
Option 2: Make the expiry field Object, this way anything can be deserialized into it.
public class CardApiMixed extends BaseCard {
private Object expiry;
//setters and getters
public String getExpiryAsString() {
return (String) this.expiry;
}
public Map<String, Object> getExpiryAsMap() {
#SuppressWarnings("unchecked")
Map<String, Object> expiry = (Map<String, Object>) this.expiry;
return expiry;
}
}
This way no matter which api you are calling, you deserialize into one class. The downside is, when retrieving expiry, you still have to know which api the data comes from, so you use correct method.
Option 3: Write custom deserializer which will resolve the field correctly, no matter which api is used. Personally i would go for this option. The deserializer is this:
public class ExpiryResolvingCardDeserializer extends StdDeserializer<CardApiResolved> {
public ExpiryResolvingCardDeserializer() {
super(CardApiResolved.class);
}
#Override
public CardApiResolved deserialize(JsonParser parser, DeserializationContext context) throws IOException {
JsonNode node = parser.getCodec().readTree(parser);
CardApiResolved card = new CardApiResolved();
card.setBrand(node.get("brand").asText());
card.setFundingMethod(node.get("fundingMethod").asText());
card.setScheme(node.get("scheme").asText());
JsonNode expiryNode = node.get("expiry");
Expiry expiry = new Expiry();
if (expiryNode.isObject()) {
//that's for api 2
expiry.setMonth(expiryNode.get("month").asText());
expiry.setYear(expiryNode.get("year").asText());
} else {
//for api 1
String text = expiryNode.asText();
//assuming format is always mmYY, you can handle it in differently if there are other options
int splitIndex = 2;
expiry.setMonth(text.substring(0, splitIndex));
expiry.setYear(text.substring(splitIndex));
}
card.setExpiry(expiry);
return card;
}
}
To summarise it, when expiry node is object, handle it like an object to get month and year data from it, if it is a string, split it to extract month and year. Like this no matter the format, expiry is always resolved to Expiry class. The card class will look like this:
#JsonDeserialize(using = ExpiryResolvingCardDeserializer.class)
public class CardApiResolved extends BaseCard {
private Expiry expiry;
//setters and getters
}
Note the JsonDeserialize annotation specifying the deserializer to use for this type. And lastly, some unit tests to play with and check results. The api responses are files in the test resources.
public class CardApiTests {
private final ObjectMapper mapper = new ObjectMapper();
#Test
public void testCardApi1() throws Exception {
InputStream inputStream = ClassLoader.getSystemResourceAsStream("card-api-1.json");
//map to CardApi1, when calling api 1
CardApi1 result = this.mapper.readValue(inputStream, CardApi1.class);
assertEquals("0139", result.getExpiry());
}
#Test
public void testCardApi2() throws Exception {
InputStream inputStream = ClassLoader.getSystemResourceAsStream("card-api-2.json");
//map to CardApi2, when calling api 2
CardApi2 result = this.mapper.readValue(inputStream, CardApi2.class);
assertEquals("1", result.getExpiry().getMonth());
assertEquals("39", result.getExpiry().getYear());
}
#Test
public void testCardApiMixed_Api1() throws Exception {
InputStream inputStream = ClassLoader.getSystemResourceAsStream("card-api-1.json");
CardApiMixed result = this.mapper.readValue(inputStream, CardApiMixed.class);
assertEquals("0139", result.getExpiryAsString());
}
#Test
public void testCardApiMixed_Api2() throws Exception {
InputStream inputStream = ClassLoader.getSystemResourceAsStream("card-api-2.json");
CardApiMixed result = this.mapper.readValue(inputStream, CardApiMixed.class);
assertEquals("1", result.getExpiryAsMap().get("month"));
assertEquals("39", result.getExpiryAsMap().get("year"));
}
#Test
public void testCardApiResolved_Api1() throws Exception {
InputStream inputStream = ClassLoader.getSystemResourceAsStream("card-api-1.json");
CardApiResolved result = this.mapper.readValue(inputStream, CardApiResolved.class);
assertEquals("01", result.getExpiry().getMonth());
assertEquals("39", result.getExpiry().getYear());
}
#Test
public void testCardApiResolved_Api2() throws Exception {
InputStream inputStream = ClassLoader.getSystemResourceAsStream("card-api-2.json");
CardApiResolved result = this.mapper.readValue(inputStream, CardApiResolved.class);
assertEquals("1", result.getExpiry().getMonth());
assertEquals("39", result.getExpiry().getYear());
}
}
Can you please share how the expiry field is prepared? like
"expiery":"0139" = month+year.
if the field is prepared like this then you can keep variable type either String or list, you have needed a minor modification. You will give an idea according to the given example.
public class App {
private static final HttpClient httpClient = HttpClient.newBuilder()
.version(HttpClient.Version.HTTP_1_1)
.connectTimeout(Duration.ofSeconds(10))
.build();
public static void main(String[] args) throws IOException, InterruptedException {
HttpRequest request = HttpRequest.newBuilder()
.GET()
.uri(URI.create("https://jsonmock.abc.com/api/articles?page=3"))
.setHeader("User-Agent", "Java 11 HttpClient Bot") // add request header
.build();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
// print response headers
HttpHeaders headers = response.headers();
headers.map().forEach((k, v) -> System.out.println(k + ":" + v));
String responseBody = response.body();
ObjectMapper mapper = new ObjectMapper();
Object newJsonNode = mapper.readValue(responseBody, Object.class);
Map<String, Object> objectMap = (Map) newJsonNode;
if (objectMap.get("expiry") instanceof List) {
//if I assume that expiry string created by appending month and year
List<Map<String, Object>> list = (List) objectMap.get("expiry");
Map<String, Object> map = list.get(0);
String exp = (String) map.get("month") + (String) map.get("year");
objectMap.put("expiry", exp);
//then parse to direct object
} else if (objectMap.get("expiry") instanceof String) {
//then cast to dto
//you can apply reverse logic here, i.e string to map
}
}
}
I simply have a Map. But it can return a Map, which may also return a Map. It's possible up to 3 to 4 nested Maps. So when I want to access a nested value, I need to do this:
((Map)((Map)((Map)CacheMap.get("id")).get("id")).get("id")).get("id")
Is there a cleaner way to do this?
The reason I'm using a Map instead of mapping it to an object is for maintainability (e.g. when there are new fields).
Note:
Map<String, Object>
It has to be Object because it won't always return a Hashmap. It may return a String or a Long.
Further clarification:
What I'm doing is I'm calling an api which returns a json response which I save as a Map.
Here's some helper methods that may help things seem cleaner and more readable:
#SuppressWarnings("unchecked")
public static Map<String, Object> getMap(Map<String, Object> map, String key) {
return (Map<String, Object>)map.get(key);
}
#SuppressWarnings("unchecked")
public static String getString(Map<String, Object> map, String key) {
return (String)map.get(key);
}
#SuppressWarnings("unchecked")
public static Integer geInteger(Map<String, Object> map, String key) {
return (Integer)map.get(key);
}
// you can add more methods for Date, Long, and any other types you know you'll get
But you would have to nest the calls:
String attrValue = getString(getMap(getMap(map, id1), id2), attrName);
Or, if you want something more funky, add the above methods as instance methods to a map impl:
public class FunkyMap extends HashMap<String, Object> {
#SuppressWarnings("unchecked")
public FunkyMap getNode(String key) {
return (FunkyMap)get(key);
}
#SuppressWarnings("unchecked")
public String getString(String key) {
return (String)get(key);
}
#SuppressWarnings("unchecked")
public Integer geInteger(String key) {
return (Integer)get(key);
}
// you can add more methods for Date, Long, and any other types you know you'll get
}
Deserialize into this class with your json library (you'll probably have to provide it with a factory method for the map class impl), then you can chain the calls more naturally:
String attrValue = map.getNode(id1).getNode(id2).getString(attrName);
The funky option is what I did for a company, and it worked a treat :)
If you don't know the depth of the JSON tree and if you worry about maintainability if new fields are added, I would recommend not to deserialize the full tree in a Map but instead use a low-level parser.
For example, if your JSON looks like the following:
{
"id": {
"id": {
"id": {
"id": 22.0
}
}
}
}
You could write something like that to get the id using Jackson:
public Object getId(String json) throws JsonParseException, IOException
{
ObjectMapper mapper = new ObjectMapper();
JsonNode root = mapper.readTree(json);
JsonNode id = root.get("id");
while (id != null && id.isObject())
{
id = id.get("id");
}
//Cannot find a JsonNode for the id
if (id == null)
{
return null;
}
//Convert id to either String or Long
if (id.isTextual())
return id.asText();
if (id.isNumber())
return id.asLong();
return null;
}
Consider the following json, getting from an public API:
anyObject : {
attributes: [
{
"name":"anyName",
"value":"anyValue"
},
{
"name":"anyName",
"value":
{
"key":"anyKey",
"label":"anyLabel"
}
}
]
}
As you can see, sometimes the value is a simple string and sometimes its an object. Is it somehow possible to deserialize those kind of json-results, to something like:
class AnyObject {
List<Attribute> attributes;
}
class Attribute {
private String key;
private String label;
}
How would I design my model to cover both cases. Is that possible ?
Despite being hard to manage as others have pointed out, you can do what you want. Add a custom deserializer to handle this situation. I rewrote your beans because I felt your Attribute class was a bit misleading. The AttributeEntry class in the object that is an entry in that "attributes" list. The ValueObject is the class that represents that "key"/"label" object. Those beans are below, but here's the custom deserializer. The idea is to check the type in the JSON, and instantiate the appropriate AttributeEntry based on its "value" type.
public class AttributeDeserializer extends JsonDeserializer<AttributeEntry> {
#Override
public AttributeEntry deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
JsonNode root = p.readValueAsTree();
String name = root.get("name").asText();
if (root.get("value").isObject()) {
// use your object mapper here, this is just an example
ValueObject attribute = new ObjectMapper().readValue(root.get("value").asText(), ValueObject.class);
return new AttributeEntry(name, attribute);
} else if (root.get("value").isTextual()) {
String stringValue = root.get("value").asText();
return new AttributeEntry(name, stringValue);
} else {
return null; // or whatever
}
}
}
Because of this ambiguous type inconvenience, you will have to do some type checking throughout your code base.
You can then add this custom deserializer to your object mapper like so:
ObjectMapper objectMapper = new ObjectMapper();
SimpleModule simpleModule = new SimpleModule();
simpleModule.addDeserializer(AttributeEntry.class, new AttributeDeserializer());
objectMapper.registerModule(simpleModule);
Here's the AttributeEntry:
public class AttributeEntry {
private String name;
private Object value;
public AttributeEntry(String name, String value) {
this.name = name;
this.value = value;
}
public AttributeEntry(String name, ValueObject attributes) {
this.name = name;
this.value = attributes;
}
/* getters/setters */
}
Here's the ValueObject:
public class ValueObject {
private String key;
private String label;
/* getters/setters */
}
I want to have Jackson always parse numbers as Long or Double.
I have a class like the following with the corresponding getters and setters:
public class Foo {
private HashMap<String, ArrayList<HashMap<String, Object>>> tables;
...
}
And some Json that looks like so:
{ "tables" :
{ "table1" :
[
{ "t1Field1" : 0,
"t1Field2" : "val2"
},
{ "t1Field1" : 1,
"t1Field2" : "val4"
}
]
}
}
Jackson will parse the values for t1Field1 as Integers/Longs and Floats/Doubles based on the size of the number. But I want to always get Longs and Doubles.
I'm almost certain I have to write a custom deserializer or parser to do this and I have looked through examples but haven't found anything that works how I would imagine. I just want to extend existing Jackson functionality and override what happens for numbers. I don't want to write a whole deserializer for Objects. I just want to do something like:
public class CustomerNumberDeserializer extends SomethingFromCoreJackson {
public Object deserialize() {
Object num;
num = super.deserialize();
if (num instanceof Integer)
return Long.valueOf(((Integer)num).intValue());
return num;
}
}
But all the Jackson classes that I thought to extend were either final or abstract and seemed to require a bunch of extra work. Is what I want possible?
After revisiting this I found the class that I wanted to extend. Hope this helps someone.
I created a custom deserializer as follows:
/**
* Custom deserializer for untyped objects that ensures integers are returned as longs
*/
public class ObjectDeserializer extends UntypedObjectDeserializer {
private static final long serialVersionUID = 7764405880012867708L;
#Override
public Object deserialize(JsonParser jp, DeserializationContext ctxt)
throws IOException {
Object out = super.deserialize(jp, ctxt);
if (out instanceof Integer) {
return Long.valueOf((Integer)out).longValue();
}
return out;
}
#Override
public Object deserializeWithType(JsonParser jp, DeserializationContext ctxt,
TypeDeserializer typeDeserializer) throws IOException {
Object out = super.deserializeWithType(jp, ctxt, typeDeserializer);
if (out instanceof Integer) {
return Long.valueOf((Integer)out).longValue();
}
return out;
}
}
And configured my object mapper to use it:
ObjectMapper om = new ObjectMapper();
SimpleModule mod = new SimpleModule().addDeserializer(Object.class, new ObjectDeserializer());
om.registerModule(mod);
Let's imagine I have the following POJO:
class Pojo {
String s;
Object o;
Map<String, String> m;
}
And at runtime, I want default serialization / deserialization for all properties except one. Typically, I want to replace a field by its ID in a database when serializing, similarly to this other question.
For example, I want to replace o by a string obtained from an external mapping (for example: object1 <=> "123" and object2 <=> "456"):
serialization: read o and replace (so if o is object1, serialize as string "123")
deserialization: read "123", query some table to get the original value of o back (i.e. object1), recreate a Pojo object with o = object1.
I understand that Modules would be one way to do that but I'm not sure how to use them while keeping the automatic BeanSerializer/Deserializer for the properties that don't need to be changed.
Can someone give an example (even contrived) or an alternative approach?
Notes:
I can't use annotations or Mixins as the changes are unknown at compile time (i.e. any properties might be changed in a way that is not determinable).
This other question points to using a CustomSerializerFactory, which seems to do the job. Unfortunately, the official site indicates that it is not the recommended approach any more and that modules should be used instead.
Edit
To be a little clearer, I can do the following with Mixins for example:
ObjectMapper mapper = new ObjectMapper(MongoBsonFactory.createFactory());
mapper.addMixInAnnotations(Pojo.class, PojoMixIn.class);
ObjectReader reader = mapper.reader(Pojo.class);
DBEncoder dbEncoder = DefaultDBEncoder.FACTORY.create();
OutputBuffer buffer = new BasicOutputBuffer();
dbEncoder.writeObject(buffer, o);
with the following Mixin:
abstract class PojoMixIn {
#JsonIgnore Object o;
}
And then add the required string to the JSON content. But I would need to know at compile time that it is the o field that needs to be replaced, which I don't.
I think #JsonSerialize and #JsonDeserialize is what you need. These annotations give you control on the serialization/deserialization of particular fields. This question shows elegant way to combine them into one annotation.
UPD. For this complex scenario you could take a look at BeanSerializerModifier/BeanDeserializerModifier classes. The idea is to modify general BeanSerializer/BeanDeserializer with your custom logic for particular fields and let basic implementation to do other stuff. Will post an example some time later.
UPD2. As I see, one of the way could be to use changeProperties method and assign your own serializer.
UPD3. Updated with working example of custom serializer. Deserialization could be done in similar way.
UPD4. Updated example with full custom serialization/deserialization. (I have used jakson-mapper-asl-1.9.8)
public class TestBeanSerializationModifiers {
static final String PropertyName = "customProperty";
static final String CustomValue = "customValue";
static final String BaseValue = "baseValue";
// Custom serialization
static class CustomSerializer extends JsonSerializer<Object> {
#Override
public void serialize(Object value, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonProcessingException {
String customValue = CustomValue; // someService.getCustomValue(value);
jgen.writeString(customValue);
}
}
static class MyBeanSerializerModifier extends BeanSerializerModifier {
#Override
public List<BeanPropertyWriter> changeProperties(SerializationConfig config, BasicBeanDescription beanDesc, List<BeanPropertyWriter> beanProperties) {
for (int i = 0; i < beanProperties.size(); i++) {
BeanPropertyWriter beanPropertyWriter = beanProperties.get(i);
if (PropertyName.equals(beanPropertyWriter.getName())) {
beanProperties.set(i, beanPropertyWriter.withSerializer(new CustomSerializer()));
}
}
return beanProperties;
}
}
// Custom deserialization
static class CustomDeserializer extends JsonDeserializer<Object> {
#Override
public Object deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {
// serialized value, 'customValue'
String serializedValue = jp.getText();
String baseValue = BaseValue; // someService.restoreOldValue(serializedValue);
return baseValue;
}
}
static class MyBeanDeserializerModifier extends BeanDeserializerModifier {
#Override
public BeanDeserializerBuilder updateBuilder(DeserializationConfig config, BasicBeanDescription beanDesc, BeanDeserializerBuilder builder) {
Iterator<SettableBeanProperty> beanPropertyIterator = builder.getProperties();
while (beanPropertyIterator.hasNext()) {
SettableBeanProperty settableBeanProperty = beanPropertyIterator.next();
if (PropertyName.equals(settableBeanProperty.getName())) {
SettableBeanProperty newSettableBeanProperty = settableBeanProperty.withValueDeserializer(new CustomDeserializer());
builder.addOrReplaceProperty(newSettableBeanProperty, true);
break;
}
}
return builder;
}
}
static class Model {
private String customProperty = BaseValue;
private String[] someArray = new String[]{"one", "two"};
public String getCustomProperty() {
return customProperty;
}
public void setCustomProperty(String customProperty) {
this.customProperty = customProperty;
}
public String[] getSomeArray() {
return someArray;
}
public void setSomeArray(String[] someArray) {
this.someArray = someArray;
}
}
public static void main(String[] args) {
SerializerFactory serializerFactory = BeanSerializerFactory
.instance
.withSerializerModifier(new MyBeanSerializerModifier());
DeserializerFactory deserializerFactory = BeanDeserializerFactory
.instance
.withDeserializerModifier(new MyBeanDeserializerModifier());
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setSerializerFactory(serializerFactory);
objectMapper.setDeserializerProvider(new StdDeserializerProvider(deserializerFactory));
try {
final String fileName = "test-serialization.json";
// Store, "customValue" -> json
objectMapper.writeValue(new File(fileName), new Model());
// Restore, "baseValue" -> model
Model model = objectMapper.readValue(new File(fileName), Model.class);
} catch (IOException e) {
e.printStackTrace();
}
}
}