Search code examples
javajsonjacksondeserializationoption-type

Jackson - Deserializing JSON into an Object having an Optional field


I have an Optional<Double> field which I am trying to deserialize from a JSON, but getting the following Exception:

Cannot construct instance of `java.util.Optional`
(although at least one Creator exists):
no BigDecimal/double/Double-argument constructor/factory method
to deserialize from Number value (2.406)

My class:

public final class TestDims implements JsonTyped, Serializable {
    private final Optional<Double> carMeters;
    
    @JsonCreator
    public TestDims(@JsonProperty("carMeters") Optional<Double> carMeters) {
        this.carMeters = carMeters;
    }
    
    @JsonProperty("carMeters")
    public Optional<Double> getCarMeters() {
        return Optional.of(carMeters.orElse(2.2d));
    }
}

Sample JSON:

{
    "carMeters": 2.406
}

Solution

  • Don't use Optional as a field

    I would advise getting rid of the Optional.

    Firstly, Optional doesn't implement Serializable hence your class is broken (because for an object to be serializable, the whole object graph should be serializable).

    Secondly, using Optional as a field is an antipattern, getters returning an optional are inconvenient, because instead of exposing the actual value they're forcing the caller to deal with an Optional.

    A possible solution is to change the type of carMeters to double:

    public final class TestDims implements JsonTyped, Serializable {
        public static final double DEFAULT_METERS = 2.2;
        
        private final double carMeters;
        
        @JsonCreator
        public TestDims(@JsonProperty("carMeters") double carMeters) {
            this.carMeters = carMeters == 0 ? DEFAULT_METERS : carMeters;
        }
        
        @JsonProperty("carMeters")
        public double getCarMeters() {
            return carMeters;
        }
    }
    

    main()

    public static void main(String[] args) throws IOException {
        String json1 = """
            {
                "carMeters": 2.406
            }""";
    
        String json2 = """
            {
                "carMeters": null
            }""";
    
        String json3 = """
            {   }
            """;
        
        ObjectMapper mapper = new ObjectMapper();
        
        System.out.println(mapper.readValue(json1, TestDims.class));
        System.out.println(mapper.readValue(json2, TestDims.class));
        System.out.println(mapper.readValue(json3, TestDims.class));
    }
    

    Output:

    TestDims{carMeters=2.406} // a regular value
    TestDims{carMeters=2.2}   // provided value is null
    TestDims{carMeters=2.2}   // the property is not present
    

    If you still want to use Optional

    If you want to use Optional as a field at all costs, then your class would not be serializable. There's no workaround.

    To let the Jackson know how to deal with Optional, you would need to add a dependency for Jackson Datatype: JDK8 module, and register it.

    If you're not using any framework, then you would need to register the module manually using ObjectMapper.registerModule() before deserializing JSON.

    With Spring Boot, it would be sufficient to add this module to the Spring's Context by using a method annotated with @Bean.

    @Bean
    public Jdk8Module jdk8Module() {
        return new Jdk8Module();
    }
    

    And it would be registered automatically while configuring ObjectMapper.

    Also note, that it doesn't make sense to use Optional<Double> since there's a type OptionalDouble wrapping a primitive.

    public final static class TestDimsss implements JsonTyped {
        public static final OptionalDouble DEFAULT_METERS = OptionalDouble.of(2.2D);
        private final OptionalDouble carMeters;
        
        @JsonCreator
        public TestDimsss(@JsonProperty("carMeters") OptionalDouble carMeters){
            this.carMeters = carMeters;
        }
        
        @JsonProperty("carMeters")
        public OptionalDouble getCarMeters() {
            return carMeters.isPresent() ? carMeters : DEFAULT_METERS;
        }
    
        @Override
        public String toString() {
            return "TestDimsss{" +
                "carMeters=" + carMeters +
                '}';
        }
    }
    

    main()

    public static void main(String[] args) throws IOException {
        String json1 = """
            {
                "carMeters": 2.406
            }""";
    
        String json2 = """
            {
                "carMeters": null
            }""";
    
        String json3 = """
            {   }
            """;
        
        ObjectMapper mapper = new ObjectMapper();
        mapper.registerModule(new Jdk8Module());
        
        System.out.println(mapper.readValue(json1, TestDims.class));
        System.out.println(mapper.readValue(json2, TestDims.class));
        System.out.println(mapper.readValue(json3, TestDims.class));
    }
    

    Output:

    TestDimsss{carMeters=OptionalDouble[2.406]}  // a regular value
    TestDimsss{carMeters=OptionalDouble.empty}   // provided value is null
    TestDimsss{carMeters=OptionalDouble.empty}   // the property is not present