2

Building API using Dropwizard framework and I came across this deserialization issue using Jackson ObjectMapper. I am using both Joda Time and Joda Money. For Joda Time, defining JodaModule was enough to resolve deserialization issue. But for Joda Money, JodaModule wasn't enough to resolve deserialization issue(Correct me if I am wrong). So, keeping JodaModule for Joda Time, I have created Joda Money specific deserializer.

public class JodaMoneyDeserializer extends JsonDeserializer<Money> {

    @Override
    public Money deserialize(JsonParser parser, DeserializationContext context) throws IOException, JsonProcessingException {
        String text = parser.getText();
        return Money.parse(text);
    }

}

For product model, I have added deserializer for Money. Let me know if I need to define anything else.

public class Product {
    ...
    private Money price;
    ...

    ...
    @JsonDeserialize(using=JodaMoneyDeserializer.class)
    public void setPrice(Money price) {
        this.price = price;
    }
    ...
}

This will attempt to parse "{" only and will throw error.

Any tips are much appreciated.

If you need additional info, let me know.

Thanks!

** UPDATED ** Here's sample JSON.

{
    "id": 15,
    "productTypeId": 1,
    "code": "XYZK",
    "name": "PRODUCT - XYZK",
    "status": true,
    "visible": true,
    "createdAt": 1400572157000,
    "updatedAt": 1398995061000,
    "description": "description of product",
    "designator": "XYZK",
    "number": "9.032",
    "ingredients": "ingredient 1, ingredient 2, ingredient 3",
    "size": null,
    "weight": 0,
    "googleProductCategory": "Health > Personal Care > Color",
    "metaDescription": null,
    "metaKeyword": null,
    "metaTitle": null,
    "price": {
        "scale": 2,
        "amount": 19.95,
        "positive": true,
        "positiveOrZero": true,
        "negativeOrZero": false,
        "amountMajor": 34,
        "amountMajorLong": 34,
        "amountMajorInt": 34,
        "amountMinor": 3495,
        "amountMinorLong": 3495,
        "amountMinorInt": 3495,
        "minorPart": 95,
        "currencyUnit": {
            "code": "USD",
            "numericCode": 840,
            "decimalPlaces": 2,
            "numeric3Code": "840",
            "countryCodes": [
                "AS",
                "US",
                "EC",
                "MP",
                "TL",
                "VI",
                "VG",
                "GU",
                "SV",
                "MH",
                "PW",
                "PR",
                "FM",
                "TC"
            ],
            "pseudoCurrency": false,
            "symbol": "$",
            "currencyCode": "USD",
            "defaultFractionDigits": 2
        },
        "zero": false,
        "negative": false
    },
    "subPrice": {
        "scale": 2,
        "amount": 0,
        "positive": false,
        "positiveOrZero": true,
        "negativeOrZero": true,
        "amountMajor": 0,
        "amountMajorLong": 0,
        "amountMajorInt": 0,
        "amountMinor": 0,
        "amountMinorLong": 0,
        "amountMinorInt": 0,
        "minorPart": 0,
        "currencyUnit": {
            "code": "USD",
            "numericCode": 840,
            "decimalPlaces": 2,
            "numeric3Code": "840",
            "countryCodes": [
                "AS",
                "US",
                "EC",
                "MP",
                "TL",
                "VI",
                "VG",
                "GU",
                "SV",
                "MH",
                "PW",
                "PR",
                "FM",
                "TC"
            ],
            "pseudoCurrency": false,
            "symbol": "$",
            "currencyCode": "USD",
            "defaultFractionDigits": 2
        },
        "zero": true,
        "negative": false
    },
    "priceGroupId": 1,
    "ignoreFulfillment": false,
    "upc": "2394823409820",
    "productSKU": "XYZK",
    "boxSKUInitial": "12345",
    "boxSKURefill": "12345",
    "urlKey": "capri-blonde",
    "isAddon": false,
    "extendedInfoJson": null
}

Exception that I am getting.

com.fasterxml.jackson.databind.JsonMappingException: Can not deserialize instance of java.lang.String out of START_OBJECT token
...
    at com.fasterxml.jackson.databind.JsonMappingException.from(JsonMappingException.java:164)
    at com.fasterxml.jackson.databind.DeserializationContext.mappingException(DeserializationContext.java:749)
    at com.fasterxml.jackson.databind.deser.std.StringDeserializer.deserialize(StringDeserializer.java:55)
    at com.fasterxml.jackson.databind.deser.std.StringDeserializer.deserialize(StringDeserializer.java:12)
    at com.fasterxml.jackson.databind.ObjectMapper._readValue(ObjectMapper.java:3025)
    at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:1637)
    at com.fasterxml.jackson.core.JsonParser.readValueAs(JsonParser.java:1346)
    at com.madisonreed.monocle.dao.mapper.deserializer.MoneyDeserializer.deserialize(MoneyDeserializer.java:26)
    at com.madisonreed.monocle.dao.mapper.deserializer.MoneyDeserializer.deserialize(MoneyDeserializer.java:18)
    at com.fasterxml.jackson.databind.deser.SettableBeanProperty.deserialize(SettableBeanProperty.java:538)
    at com.fasterxml.jackson.databind.deser.BeanDeserializer._deserializeUsingPropertyBased(BeanDeserializer.java:332)
    at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.deserializeFromObjectUsingNonDefault(BeanDeserializerBase.java:1058)
    at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserializeFromObject(BeanDeserializer.java:268)
    at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:124)
    at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:3053)
    at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:2148)
    at com.madisonreed.monocle.resources.ProductResourceTest.testGetProduct(ProductResourceTest.java:127)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:606)
    at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:45)
    at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:15)
    at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:42)
    at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:20)
    at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:28)
    at org.junit.internal.runners.statements.RunAfters.evaluate(RunAfters.java:30)
    at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:263)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:68)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:47)
    at org.junit.runners.ParentRunner$3.run(ParentRunner.java:231)
    at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:60)
    at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:229)
    at org.junit.runners.ParentRunner.access$000(ParentRunner.java:50)
    at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:222)
    at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:28)
    at org.junit.internal.runners.statements.RunAfters.evaluate(RunAfters.java:30)
    at io.dropwizard.testing.junit.DropwizardAppRule$1.evaluate(DropwizardAppRule.java:55)
    at org.junit.rules.RunRules.evaluate(RunRules.java:18)
    at org.junit.runners.ParentRunner.run(ParentRunner.java:300)
    at org.apache.maven.surefire.junit4.JUnit4TestSet.execute(JUnit4TestSet.java:53)
    at org.apache.maven.surefire.junit4.JUnit4Provider.executeTestSet(JUnit4Provider.java:123)
    at org.apache.maven.surefire.junit4.JUnit4Provider.invoke(JUnit4Provider.java:104)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:606)
    at org.apache.maven.surefire.util.ReflectionUtils.invokeMethodWithArray(ReflectionUtils.java:164)
    at org.apache.maven.surefire.booter.ProviderFactory$ProviderProxy.invoke(ProviderFactory.java:110)
    at org.apache.maven.surefire.booter.SurefireStarter.invokeProvider(SurefireStarter.java:175)
    at org.apache.maven.surefire.booter.SurefireStarter.runSuitesInProcessWhenForked(SurefireStarter.java:107)
    at org.apache.maven.surefire.booter.ForkedBooter.main(ForkedBooter.java:68)

This is how I register deserializer.

SimpleModule module = new SimpleModule();
module.addDeserializer(Money.class, new MoneyDeserializer());
mapper = new ObjectMapper();
mapper.registerModule(module);
mapper.registerModule(new JodaModule());

This is where I deserialize my product object.

public void testGetProduct() {
    System.out.println("getProduct");
    ClientResponse response = client.resource(String.format("http://localhost:%d/api/v1/products/15", RULE.getLocalPort())).get(ClientResponse.class);
    Product product = null;
    Boolean validJson = false;
    if (response.getStatus() == 200) {
        String productJSON = response.getEntity(String.class);
        validJson = JacksonJsonUtility.isValidJSON(productJSON);
        System.out.println(productJSON);
        try {
            product = mapper.readValue(productJSON, Product.class);
        } catch (IOException ex) {
            ex.printStackTrace();
            Logger.getLogger(ProductResourceTest.class.getName()).log(Level.SEVERE, null, ex);
        }
    }
    assertTrue(validJson);
}
davnicwil
  • 28,487
  • 16
  • 107
  • 123
Jerry H
  • 105
  • 2
  • 7

2 Answers2

6

Here is an example that register serializer and deserializer for the Joda Money type. All the Money objects are converted to a JSON string.

public class JacksonJodaMoney {

    public static class Product {
        public final Money price;

        @JsonCreator
        public Product(@JsonProperty("price") Money price) {
            this.price = price;
        }

        @Override
        public String toString() {
            return "Product{" +
                    "price=" + price +
                    '}';
        }
    }

    private static class MoneySerializer extends StdSerializer<Money> {
        protected MoneySerializer() {
            super(Money.class);
        }

        @Override
        public void serialize(Money value, JsonGenerator jgen, SerializerProvider provider) throws IOException {
            jgen.writeString(value.toString());
        }
    }

    private static class MoneyDeserializer extends StdDeserializer<Money> {
        protected MoneyDeserializer() {
            super(Money.class);
        }

        @Override
        public Money deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException {
            return Money.parse(jp.readValueAs(String.class));
        }
    }

    public static void main(String[] args) throws IOException {
        ObjectMapper mapper = new ObjectMapper();
        Product value = new Product(Money.of(CurrencyUnit.EUR, 40.55));
        SimpleModule module = new SimpleModule();
        module.addDeserializer(Money.class, new MoneyDeserializer());
        module.addSerializer(Money.class, new MoneySerializer());
        mapper.registerModule(module);
        String json = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(value);
        System.out.println(json);
        System.out.println(mapper.readValue(json, Product.class));
    }
}

Output:

{
  "price" : "EUR 40.55"
}
Product{price=EUR 40.55}
Alexey Gavrilov
  • 10,593
  • 2
  • 38
  • 48
  • Thanks for the great answer. Unfortunately, this product model doesn't quite fit my problem. It is my mistake not listing all possible product attribute for the complete answer. I was hoping that deserializer will be used to deserialize Money attribute of product automagically. Product model has many other attributes and JSON will have all those attributes of the product listed and will throw "Can not deserialize instance of java.lang.String out of START_OBJECT token. Any tips are appreciated. Thanks. – Jerry H May 27 '14 at 18:27
  • Can you show an example of JSON with all those attributes and an exception stack trace? – Alexey Gavrilov May 27 '14 at 19:15
  • Since I couldn't add what you have asked as part of comment, I have updated my question. Thanks. – Jerry H May 27 '14 at 20:48
  • OK. The format of money field in your JSON looks suspicious. Which serializer class are you using? Try the one from my answer. – Alexey Gavrilov May 27 '14 at 21:03
  • Ok, I implemented serializer again and this time it worked. Yes, you were right about serializer. I was using Joda Money default serializer which had all that JSON tag and I didn't want to use different serializer since it supposed to work, but it didn't. When I used simple serializer, it worked. I hope Joda Money make deserializer like the one that Joda Time has... Thanks! – Jerry H May 27 '14 at 22:26
0

You can use this MoneyDeserializer and then register this deserializer

public class MoneyDeserializer extends JsonDeserializer<Money> {

@Override
public Money deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException {

    String currencyCode = "GBP";
    ObjectMapper mapper = (ObjectMapper) jp.getCodec();
    ObjectNode root = mapper.readTree(jp);
    DoubleNode amountNode = (DoubleNode) root.findValue("amount");
    String amount = null;
    if (null != amountNode) {
        amount = amountNode.asText();
    }
    JsonNode currencyUnitNode = root.get("currencyUnit");
    JsonNode currencyCodeNode = currencyUnitNode.get("currencyCode");
    currencyCode = currencyCodeNode.textValue();
    if (StringUtils.isBlank(amount) || StringUtils.isBlank(currencyCode)) {
        throw new IOException("unable to parse json");
    }
    return Money.parse(currencyCode + " " + amount);
}}

and then register the deserializer:

    ObjectMapper mapper = new ObjectMapper();
    JodaModule module = new JodaModule();
    module.addDeserializer(Money.class, new MoneyDeserializer());
    mapper.registerModule(module);
    YourClass yourclass= mapper.readValue(jsonString, YourClass.class);