From 884af2e48d3f265ad62b95fa003e0e3a9afc1ae0 Mon Sep 17 00:00:00 2001 From: Dave Moten Date: Thu, 11 Apr 2024 02:36:09 +1000 Subject: [PATCH] Deserialize integer/long JSON into double single-arg constructors (#4474) --- release-notes/CREDITS-2.x | 5 + release-notes/VERSION-2.x | 3 + .../deser/std/StdValueInstantiator.java | 26 ++- .../deser/std/StdValueInstantiatorTest.java | 178 ++++++++++++++++++ 4 files changed, 211 insertions(+), 1 deletion(-) diff --git a/release-notes/CREDITS-2.x b/release-notes/CREDITS-2.x index 8dd6c672ea..93d42cdc8f 100644 --- a/release-notes/CREDITS-2.x +++ b/release-notes/CREDITS-2.x @@ -1771,6 +1771,11 @@ Oddbjørn Kvalsund (oddbjornkvalsund@github) in `DeserializerCache` to avoid deadlock on pinning (2.17.1) +David Moten (davidmoten@github) + * Contributed #4453: Allow JSON Integer to deserialize into a single-arg constructor of + parameter type `double` + (2.18.0) + Teodor Danciu (teodord@github) * Reported #4464: When `Include.NON_DEFAULT` setting is used, `isEmpty()` method is not called on the serializer diff --git a/release-notes/VERSION-2.x b/release-notes/VERSION-2.x index 99ff4b9151..f807b5a704 100644 --- a/release-notes/VERSION-2.x +++ b/release-notes/VERSION-2.x @@ -6,6 +6,9 @@ Project: jackson-databind 2.18.0 (not yet released) +#4453: Allow JSON Integer to deserialize into a single-arg constructor of + parameter type `double` + (contributed by David M) #4456: Rework locking in `DeserializerCache` (contributed by @pjfanning) #4458: Rework synchronized block from `BeanDeserializerBase` diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/std/StdValueInstantiator.java b/src/main/java/com/fasterxml/jackson/databind/deser/std/StdValueInstantiator.java index 09630b36ab..258c8ff937 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/std/StdValueInstantiator.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/std/StdValueInstantiator.java @@ -383,7 +383,18 @@ arg, rewrapCtorProblem(ctxt, t0) ); } } - + + if (_fromDoubleCreator != null) { + Object arg = Double.valueOf(value); + try { + return _fromDoubleCreator.call1(arg); + } catch (Exception t0) { + return ctxt.handleInstantiationProblem(_fromDoubleCreator.getDeclaringClass(), + arg, rewrapCtorProblem(ctxt, t0) + ); + } + } + return super.createFromInt(ctxt, value); } @@ -411,6 +422,19 @@ arg, rewrapCtorProblem(ctxt, t0) ); } } + + // [databind#4453]: Note: can lose precision (since double is 64-bits of which + // only part is for mantissa). But already the case with regular properties. + if (_fromDoubleCreator != null) { + Object arg = Double.valueOf(value); + try { + return _fromDoubleCreator.call1(arg); + } catch (Exception t0) { + return ctxt.handleInstantiationProblem(_fromDoubleCreator.getDeclaringClass(), + arg, rewrapCtorProblem(ctxt, t0) + ); + } + } return super.createFromLong(ctxt, value); } diff --git a/src/test/java/com/fasterxml/jackson/databind/deser/std/StdValueInstantiatorTest.java b/src/test/java/com/fasterxml/jackson/databind/deser/std/StdValueInstantiatorTest.java index 9e9909a113..f977299ff5 100644 --- a/src/test/java/com/fasterxml/jackson/databind/deser/std/StdValueInstantiatorTest.java +++ b/src/test/java/com/fasterxml/jackson/databind/deser/std/StdValueInstantiatorTest.java @@ -1,15 +1,23 @@ package com.fasterxml.jackson.databind.deser.std; import java.math.BigDecimal; +import java.math.BigInteger; import org.junit.jupiter.api.Test; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.exc.ValueInstantiationException; + import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; // [databind#2978] public class StdValueInstantiatorTest { + private static final long LONG_TEST_VALUE = 12345678901L; + @Test public void testDoubleValidation_valid() { assertEquals(0d, StdValueInstantiator.tryConvertToDouble(BigDecimal.ZERO)); @@ -23,4 +31,174 @@ public void testDoubleValidation_invalid() { BigDecimal value = BigDecimal.valueOf(Double.MAX_VALUE).add(BigDecimal.valueOf(Double.MAX_VALUE)); assertNull(StdValueInstantiator.tryConvertToDouble(value)); } + + @Test + public void testJsonIntegerToDouble() throws Exception { + ObjectMapper m = new ObjectMapper(); + Stuff a = m.readValue("5", Stuff.class); + assertEquals(5, a.value); + } + + @Test + public void testJsonLongToDouble() throws Exception { + ObjectMapper m = new ObjectMapper(); + assertTrue(LONG_TEST_VALUE > Integer.MAX_VALUE); + Stuff a = m.readValue(String.valueOf(LONG_TEST_VALUE), Stuff.class); + assertEquals(LONG_TEST_VALUE, a.value); + } + + static class Stuff { + final double value; + + Stuff(double value) { + this.value = value; + } + } + + @Test + public void testJsonIntegerDeserializationPrefersInt() throws Exception { + ObjectMapper m = new ObjectMapper(); + A a = m.readValue("5", A.class); + assertEquals(1, a.creatorType); + } + + static class A { + final int creatorType; + + A(int value) { + this.creatorType = 1; + } + + A(long value) { + this.creatorType = 2; + } + + A(BigInteger value) { + this.creatorType = 3; + } + + A(double value) { + this.creatorType = 4; + } + } + + @Test + public void testJsonIntegerDeserializationPrefersLong() throws Exception { + ObjectMapper m = new ObjectMapper(); + B a = m.readValue("5", B.class); + assertEquals(2, a.creatorType); + } + + static class B { + final int creatorType; + + B(long value) { + this.creatorType = 2; + } + + B(BigInteger value) { + this.creatorType = 3; + } + + B(double value) { + this.creatorType = 4; + } + } + + @Test + public void testJsonIntegerDeserializationPrefersBigInteger() throws Exception { + ObjectMapper m = new ObjectMapper(); + C a = m.readValue("5", C.class); + assertEquals(3, a.creatorType); + } + + static class C { + final int creatorType; + + C(BigInteger value) { + this.creatorType = 3; + } + + C(double value) { + this.creatorType = 4; + } + } + + @Test + public void testJsonLongDeserializationPrefersLong() throws Exception { + ObjectMapper m = new ObjectMapper(); + A2 a = m.readValue(String.valueOf(LONG_TEST_VALUE), A2.class); + assertEquals(2, a.creatorType); + } + + static class A2 { + final int creatorType; + + A2(int value) { + this.creatorType = 1; + } + + A2(long value) { + this.creatorType = 2; + } + + A2(BigInteger value) { + this.creatorType = 3; + } + + A2(double value) { + this.creatorType = 4; + } + } + + @Test + public void testJsonLongDeserializationPrefersBigInteger() throws Exception { + ObjectMapper m = new ObjectMapper(); + B2 a = m.readValue(String.valueOf(LONG_TEST_VALUE), B2.class); + assertEquals(3, a.creatorType); + } + + static class B2 { + final int creatorType; + + B2(BigInteger value) { + this.creatorType = 3; + } + + B2(double value) { + this.creatorType = 4; + } + } + + @Test + public void testJsonIntegerIntoDoubleConstructorThrows() throws Exception { + ObjectMapper m = new ObjectMapper(); + try { + m.readValue("5", D.class); + fail(); + } catch (ValueInstantiationException e) { + assertTrue(e.getCause() instanceof IllegalArgumentException); + assertEquals("boo", e.getCause().getMessage()); + } + } + + static final class D { + + D(double value) { + throw new IllegalArgumentException("boo"); + } + } + + @Test + public void testJsonLongIntoDoubleConstructorThrows() throws Exception { + ObjectMapper m = new ObjectMapper(); + try { + m.readValue(String.valueOf(LONG_TEST_VALUE), D.class); + fail(); + } catch (ValueInstantiationException e) { + assertTrue(e.getCause() instanceof IllegalArgumentException); + assertEquals("boo", e.getCause().getMessage()); + } + } + }