diff --git a/release-notes/CREDITS b/release-notes/CREDITS index 55a7b01f2f..301f06b74b 100644 --- a/release-notes/CREDITS +++ b/release-notes/CREDITS @@ -271,3 +271,9 @@ Michal Letynski (mletynski@github) * Suggested #296: Serialization of transient fields with public getters (add MapperFeature.PROPAGATE_TRANSIENT_MARKER) (2.6.0) + +Jeff Schnitzer (stickfigure@github) + * Suggested #504: Add `DeserializationFeature.USE_LONG_FOR_INTS` + (2.6.0) + + diff --git a/release-notes/VERSION b/release-notes/VERSION index fdb0c5c289..157887b6ed 100644 --- a/release-notes/VERSION +++ b/release-notes/VERSION @@ -14,6 +14,8 @@ Project: jackson-databind #312: Support Type Id mappings where two ids map to same Class #348: ObjectMapper.valueToTree does not work with @JsonRawValue (reported by Chris P, pimlottc@github) +#504: Add `DeserializationFeature.USE_LONG_FOR_INTS` + (suggested by Jeff S) #649: Make `BeanDeserializer` use new `parser.nextFieldName()` and `.hasTokenId()` methods #664: Add `DeserializationFeature.ACCEPT_FLOAT_AS_INT` to prevent coercion of floating point numbers int `int`/`long`/`Integer`/`Long` @@ -51,6 +53,7 @@ Project: jackson-databind timezone id for date/time values (as opposed to timezone offset) #795: Converter annotation not honored for abstract types (reported by myrosia@github) +#797: `JsonNodeFactory` method `numberNode(long)` produces `IntNode` for small numbers - Remove old cglib compatibility tests; cause problems in Eclipse 2.5.4 (not yet released) diff --git a/src/main/java/com/fasterxml/jackson/databind/DeserializationConfig.java b/src/main/java/com/fasterxml/jackson/databind/DeserializationConfig.java index cbee948b31..653d29bb41 100644 --- a/src/main/java/com/fasterxml/jackson/databind/DeserializationConfig.java +++ b/src/main/java/com/fasterxml/jackson/databind/DeserializationConfig.java @@ -648,7 +648,7 @@ public final boolean isEnabled(JsonParser.Feature f, JsonFactory factory) { } /** - * "Bulk" access method for checking that all features specified by + * Bulk access method for checking that all features specified by * mask are enabled. * * @since 2.3 @@ -657,6 +657,20 @@ public final boolean hasDeserializationFeatures(int featureMask) { return (_deserFeatures & featureMask) == featureMask; } + /** + * Bulk access method for checking that at least one of features specified by + * mask is enabled. + * + * @since 2.6 + */ + public final boolean hasSomeOfFeatures(int featureMask) { + return (_deserFeatures & featureMask) != 0; + } + + /** + * Bulk access method for getting the bit mask of all {@link DeserializationFeature}s + * that are enabled. + */ public final int getDeserializationFeatures() { return _deserFeatures; } diff --git a/src/main/java/com/fasterxml/jackson/databind/DeserializationContext.java b/src/main/java/com/fasterxml/jackson/databind/DeserializationContext.java index 35ca839769..6ad9b758d2 100644 --- a/src/main/java/com/fasterxml/jackson/databind/DeserializationContext.java +++ b/src/main/java/com/fasterxml/jackson/databind/DeserializationContext.java @@ -291,13 +291,33 @@ public final boolean isEnabled(DeserializationFeature feat) { } /** - * "Bulk" access method for checking that all features specified by + * Bulk access method for getting the bit mask of all {@link DeserializationFeature}s + * that are enabled. + * + * @since 2.6 + */ + public final int getDeserializationFeatures() { + return _featureFlags; + } + + /** + * Bulk access method for checking that all features specified by * mask are enabled. * * @since 2.3 */ public final boolean hasDeserializationFeatures(int featureMask) { - return _config.hasDeserializationFeatures(featureMask); + return (_featureFlags & featureMask) == featureMask; + } + + /** + * Bulk access method for checking that at least one of features specified by + * mask is enabled. + * + * @since 2.6 + */ + public final boolean hasSomeOfFeatures(int featureMask) { + return (_featureFlags & featureMask) != 0; } /** diff --git a/src/main/java/com/fasterxml/jackson/databind/DeserializationFeature.java b/src/main/java/com/fasterxml/jackson/databind/DeserializationFeature.java index e36574aabd..c0f2602c32 100644 --- a/src/main/java/com/fasterxml/jackson/databind/DeserializationFeature.java +++ b/src/main/java/com/fasterxml/jackson/databind/DeserializationFeature.java @@ -53,13 +53,35 @@ public enum DeserializationFeature implements ConfigFeature * which is either {@link Integer}, {@link Long} or * {@link java.math.BigInteger}, depending on number of digits. *

- * Feature is disabled by default, meaning that "untyped" floating - * point numbers will by default be deserialized using whatever + * Feature is disabled by default, meaning that "untyped" integral + * numbers will by default be deserialized using whatever * is the most compact integral type, to optimize efficiency. */ USE_BIG_INTEGER_FOR_INTS(false), - // [JACKSON-652] + /** + * Feature that determines how "small" JSON integral (non-floating-point) + * numbers -- ones that fit in 32-bit signed integer (`int`) -- are bound + * when target type is loosely typed as {@link Object} or {@link Number} + * (or within untyped {@link java.util.Map} or {@link java.util.Collection} context). + * If enabled, such values will be deserialized as {@link java.lang.Long}; + * if disabled, they will be deserialized as "smallest" available type, + * {@link Integer}. + * In addition, if enabled, trying to bind values that do not fit in {@link java.lang.Long} + * will throw a {@link com.fasterxml.jackson.core.JsonProcessingException}. + *

+ * Note: if {@link #USE_BIG_INTEGER_FOR_INTS} is enabled, it has precedence + * over this setting, forcing use of {@link java.math.BigInteger} for all + * integral values. + *

+ * Feature is disabled by default, meaning that "untyped" integral + * numbers will by default be deserialized using {@link java.lang.Integer} + * if value fits. + * + * @since 2.6 + */ + USE_LONG_FOR_INTS(false), + /** * Feature that determines whether JSON Array is mapped to * Object[] or List<Object> when binding @@ -402,4 +424,4 @@ private DeserializationFeature(boolean defaultState) { * @since 2.5 */ public boolean enabledIn(int flags) { return (flags & _mask) != 0; } -} \ No newline at end of file +} diff --git a/src/main/java/com/fasterxml/jackson/databind/ObjectMapper.java b/src/main/java/com/fasterxml/jackson/databind/ObjectMapper.java index 5846a2e26c..3033a09d75 100644 --- a/src/main/java/com/fasterxml/jackson/databind/ObjectMapper.java +++ b/src/main/java/com/fasterxml/jackson/databind/ObjectMapper.java @@ -1271,7 +1271,7 @@ public ObjectMapper setSerializationInclusion(JsonInclude.Include incl) { * Method for specifying {@link PrettyPrinter} to use when "default pretty-printing" * is enabled (by enabling {@link SerializationFeature#INDENT_OUTPUT}) * - * @param pp + * @param pp Pretty printer to use by default. * * @return This mapper, useful for call-chaining * diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/std/JsonNodeDeserializer.java b/src/main/java/com/fasterxml/jackson/databind/deser/std/JsonNodeDeserializer.java index 2046f2d4da..0678620821 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/std/JsonNodeDeserializer.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/std/JsonNodeDeserializer.java @@ -331,15 +331,26 @@ protected final JsonNode deserializeAny(JsonParser p, DeserializationContext ctx protected final JsonNode _fromInt(JsonParser p, DeserializationContext ctxt, JsonNodeFactory nodeFactory) throws IOException { - JsonParser.NumberType nt = p.getNumberType(); - if (nt == JsonParser.NumberType.BIG_INTEGER - || ctxt.isEnabled(DeserializationFeature.USE_BIG_INTEGER_FOR_INTS)) { - return nodeFactory.numberNode(p.getBigIntegerValue()); + JsonParser.NumberType nt; + int feats = ctxt.getDeserializationFeatures(); + if ((feats & F_MASK_INT_COERCIONS) != 0) { + if (DeserializationFeature.USE_BIG_INTEGER_FOR_INTS.enabledIn(feats)) { + nt = JsonParser.NumberType.BIG_INTEGER; + } else if (DeserializationFeature.USE_LONG_FOR_INTS.enabledIn(feats)) { + nt = JsonParser.NumberType.LONG; + } else { + nt = p.getNumberType(); + } + } else { + nt = p.getNumberType(); } if (nt == JsonParser.NumberType.INT) { return nodeFactory.numberNode(p.getIntValue()); } - return nodeFactory.numberNode(p.getLongValue()); + if (nt == JsonParser.NumberType.LONG) { + return nodeFactory.numberNode(p.getLongValue()); + } + return nodeFactory.numberNode(p.getBigIntegerValue()); } protected final JsonNode _fromFloat(JsonParser p, DeserializationContext ctxt, diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/std/NumberDeserializers.java b/src/main/java/com/fasterxml/jackson/databind/deser/std/NumberDeserializers.java index 50f3c366ea..0a1df4ca61 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/std/NumberDeserializers.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/std/NumberDeserializers.java @@ -410,21 +410,24 @@ public Double deserializeWithType(JsonParser jp, DeserializationContext ctxt, @SuppressWarnings("serial") @JacksonStdImpl public static class NumberDeserializer - extends StdScalarDeserializer + extends StdScalarDeserializer { public final static NumberDeserializer instance = new NumberDeserializer(); - public NumberDeserializer() { super(Number.class); } + public NumberDeserializer() { + super(Number.class); + } @Override - public Number deserialize(JsonParser p, DeserializationContext ctxt) throws IOException + public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { switch (p.getCurrentTokenId()) { case JsonTokenId.ID_NUMBER_INT: - if (ctxt.isEnabled(DeserializationFeature.USE_BIG_INTEGER_FOR_INTS)) { - return p.getBigIntegerValue(); + if (ctxt.hasSomeOfFeatures(F_MASK_INT_COERCIONS)) { + return _coerceIntegral(p, ctxt); } return p.getNumberValue(); + case JsonTokenId.ID_NUMBER_FLOAT: if (ctxt.isEnabled(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS)) { return p.getDecimalValue(); @@ -462,8 +465,10 @@ public Number deserialize(JsonParser p, DeserializationContext ctxt) throws IOEx return new BigInteger(text); } long value = Long.parseLong(text); - if (value <= Integer.MAX_VALUE && value >= Integer.MIN_VALUE) { - return Integer.valueOf((int) value); + if (!ctxt.isEnabled(DeserializationFeature.USE_LONG_FOR_INTS)) { + if (value <= Integer.MAX_VALUE && value >= Integer.MIN_VALUE) { + return Integer.valueOf((int) value); + } } return Long.valueOf(value); } catch (IllegalArgumentException iae) { @@ -472,7 +477,7 @@ public Number deserialize(JsonParser p, DeserializationContext ctxt) throws IOEx case JsonTokenId.ID_START_ARRAY: if (ctxt.isEnabled(DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS)) { p.nextToken(); - final Number value = deserialize(p, ctxt); + final Object value = deserialize(p, ctxt); if (p.nextToken() != JsonToken.END_ARRAY) { throw ctxt.wrongTokenException(p, JsonToken.END_ARRAY, "Attempted to unwrap single value array for single '" + _valueClass.getName() + "' value but there was more than a single value in the array" diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/std/StdDeserializer.java b/src/main/java/com/fasterxml/jackson/databind/deser/std/StdDeserializer.java index 76888bc7c9..866454b059 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/std/StdDeserializer.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/std/StdDeserializer.java @@ -24,6 +24,17 @@ public abstract class StdDeserializer { private static final long serialVersionUID = 1L; + /** + * Bitmask that covers {@link DeserializationFeature#USE_BIG_INTEGER_FOR_INTS} + * and {@link DeserializationFeature#USE_LONG_FOR_INTS}, used for more efficient + * cheks when coercing integral values for untyped deserialization. + * + * @since 2.6 + */ + protected final static int F_MASK_INT_COERCIONS = + DeserializationFeature.USE_BIG_INTEGER_FOR_INTS.getMask() + | DeserializationFeature.USE_LONG_FOR_INTS.getMask(); + /** * Type of values this deserializer handles: sometimes * exact types, other time most specific supertype of @@ -884,6 +895,34 @@ protected final boolean _isPosInf(String text) { } protected final boolean _isNaN(String text) { return "NaN".equals(text); } + + /* + /**************************************************** + /* Helper methods for sub-classes, coercions + /**************************************************** + */ + + /** + * Helper method called in case where an integral number is encountered, but + * config settings suggest that a coercion may be needed to "upgrade" + * {@link java.lang.Number} into "bigger" type like {@link java.lang.Long} or + * {@link java.math.BigInteger} + * + * @see {@link DeserializationFeature#USE_BIG_INTEGER_FOR_INTS}, {@link DeserializationFeature#USE_LONG_FOR_INTS} + * + * @since 2.6 + */ + protected Object _coerceIntegral(JsonParser p, DeserializationContext ctxt) throws IOException + { + int feats = ctxt.getDeserializationFeatures(); + if (DeserializationFeature.USE_BIG_INTEGER_FOR_INTS.enabledIn(feats)) { + return p.getBigIntegerValue(); + } + if (DeserializationFeature.USE_LONG_FOR_INTS.enabledIn(feats)) { + return p.getLongValue(); + } + return p.getBigIntegerValue(); // should be optimal, whatever it is + } /* /**************************************************** diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/std/UntypedObjectDeserializer.java b/src/main/java/com/fasterxml/jackson/databind/deser/std/UntypedObjectDeserializer.java index 6ca9c9ceba..55c7434f6f 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/std/UntypedObjectDeserializer.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/std/UntypedObjectDeserializer.java @@ -240,11 +240,11 @@ public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOEx if (_numberDeserializer != null) { return _numberDeserializer.deserialize(p, ctxt); } - /* [JACKSON-100]: caller may want to get all integral values - * returned as BigInteger, for consistency + /* Caller may want to get all integral values returned as {@link java.math.BigInteger}, + * or {@link java.lang.Long} for consistency */ - if (ctxt.isEnabled(DeserializationFeature.USE_BIG_INTEGER_FOR_INTS)) { - return p.getBigIntegerValue(); // should be optimal, whatever it is + if (ctxt.hasSomeOfFeatures(F_MASK_INT_COERCIONS)) { + return _coerceIntegral(p, ctxt); } return p.getNumberValue(); // should be optimal, whatever it is @@ -304,15 +304,11 @@ public Object deserializeWithType(JsonParser p, DeserializationContext ctxt, Typ if (_numberDeserializer != null) { return _numberDeserializer.deserialize(p, ctxt); } - // For [JACKSON-100], see above: - if (ctxt.isEnabled(DeserializationFeature.USE_BIG_INTEGER_FOR_INTS)) { - return p.getBigIntegerValue(); + // May need coercion to "bigger" types: + if (ctxt.hasSomeOfFeatures(F_MASK_INT_COERCIONS)) { + return _coerceIntegral(p, ctxt); } - /* and as per [JACKSON-839], allow "upgrade" to bigger types: out-of-range - * entries can not be produced without type, so this should "just work", - * even if it is bit unclean - */ - return p.getNumberValue(); + return p.getNumberValue(); // should be optimal, whatever it is case JsonTokenId.ID_NUMBER_FLOAT: if (_numberDeserializer != null) { @@ -484,23 +480,23 @@ public static class Vanilla public final static Vanilla std = new Vanilla(); public Vanilla() { super(Object.class); } - + @Override - public Object deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException + public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { - switch (jp.getCurrentTokenId()) { + switch (p.getCurrentTokenId()) { case JsonTokenId.ID_START_OBJECT: { - JsonToken t = jp.nextToken(); + JsonToken t = p.nextToken(); if (t == JsonToken.END_OBJECT) { return new LinkedHashMap(2); } } case JsonTokenId.ID_FIELD_NAME: - return mapObject(jp, ctxt); + return mapObject(p, ctxt); case JsonTokenId.ID_START_ARRAY: { - JsonToken t = jp.nextToken(); + JsonToken t = p.nextToken(); if (t == JsonToken.END_ARRAY) { // and empty one too if (ctxt.isEnabled(DeserializationFeature.USE_JAVA_ARRAY_FOR_JSON_ARRAY)) { return NO_OBJECTS; @@ -509,25 +505,25 @@ public Object deserialize(JsonParser jp, DeserializationContext ctxt) throws IOE } } if (ctxt.isEnabled(DeserializationFeature.USE_JAVA_ARRAY_FOR_JSON_ARRAY)) { - return mapArrayToArray(jp, ctxt); + return mapArrayToArray(p, ctxt); } - return mapArray(jp, ctxt); + return mapArray(p, ctxt); case JsonTokenId.ID_EMBEDDED_OBJECT: - return jp.getEmbeddedObject(); + return p.getEmbeddedObject(); case JsonTokenId.ID_STRING: - return jp.getText(); + return p.getText(); case JsonTokenId.ID_NUMBER_INT: - if (ctxt.isEnabled(DeserializationFeature.USE_BIG_INTEGER_FOR_INTS)) { - return jp.getBigIntegerValue(); // should be optimal, whatever it is + if (ctxt.hasSomeOfFeatures(F_MASK_INT_COERCIONS)) { + return _coerceIntegral(p, ctxt); } - return jp.getNumberValue(); // should be optimal, whatever it is + return p.getNumberValue(); // should be optimal, whatever it is case JsonTokenId.ID_NUMBER_FLOAT: if (ctxt.isEnabled(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS)) { - return jp.getDecimalValue(); + return p.getDecimalValue(); } - return Double.valueOf(jp.getDoubleValue()); + return Double.valueOf(p.getDoubleValue()); case JsonTokenId.ID_TRUE: return Boolean.TRUE; diff --git a/src/main/java/com/fasterxml/jackson/databind/node/JsonNodeFactory.java b/src/main/java/com/fasterxml/jackson/databind/node/JsonNodeFactory.java index 7de1b88f55..0d0d5744cc 100644 --- a/src/main/java/com/fasterxml/jackson/databind/node/JsonNodeFactory.java +++ b/src/main/java/com/fasterxml/jackson/databind/node/JsonNodeFactory.java @@ -174,9 +174,6 @@ public ValueNode numberNode(Integer value) { */ @Override public NumericNode numberNode(long v) { - if (_inIntRange(v)) { - return IntNode.valueOf((int) v); - } return LongNode.valueOf(v); } @@ -190,9 +187,7 @@ public ValueNode numberNode(Long value) { if (value == null) { return nullNode(); } - long l = value.longValue(); - return _inIntRange(l) - ? IntNode.valueOf((int) l) : LongNode.valueOf(l); + return LongNode.valueOf(value.longValue()); } /** diff --git a/src/test/java/com/fasterxml/jackson/databind/deser/UntypedNumbersTest.java b/src/test/java/com/fasterxml/jackson/databind/deser/UntypedNumbersTest.java index 110dd15a5d..2de43898d6 100644 --- a/src/test/java/com/fasterxml/jackson/databind/deser/UntypedNumbersTest.java +++ b/src/test/java/com/fasterxml/jackson/databind/deser/UntypedNumbersTest.java @@ -21,7 +21,7 @@ public void testIntAsNumber() throws Exception /* Even if declared as 'generic' type, should return using most * efficient type... here, Integer */ - Number result = MAPPER.readValue(new StringReader(" 123 "), Number.class); + Number result = MAPPER.readValue(" 123 ", Number.class); assertEquals(Integer.valueOf(123), result); } @@ -52,14 +52,19 @@ public void testIntTypeOverride() throws Exception BigInteger exp = BigInteger.valueOf(123L); // first test as any Number - Number result = r.forType(Number.class).readValue(new StringReader(" 123 ")); + Number result = r.forType(Number.class).readValue(" 123 "); assertEquals(BigInteger.class, result.getClass()); assertEquals(exp, result); // then as any Object - /*Object value =*/ r.forType(Object.class).readValue(new StringReader("123")); + /*Object value =*/ r.forType(Object.class).readValue("123"); assertEquals(BigInteger.class, result.getClass()); assertEquals(exp, result); + + // and as JsonNode + JsonNode node = r.readTree(" 123"); + assertTrue(node.isBigInteger()); + assertEquals(123, node.asInt()); } public void testDoubleAsNumber() throws Exception @@ -82,6 +87,10 @@ public void testFpTypeOverrideSimple() throws Exception Object value = r.forType(Object.class).readValue(dec.toString()); assertEquals(BigDecimal.class, result.getClass()); assertEquals(dec, value); + + JsonNode node = r.readTree(dec.toString()); + assertTrue(node.isBigDecimal()); + assertEquals(dec.doubleValue(), node.asDouble()); } public void testFpTypeOverrideStructured() throws Exception @@ -108,6 +117,21 @@ public void testFpTypeOverrideStructured() throws Exception // [databind#504] public void testForceIntsToLongs() throws Exception { - + ObjectReader r = MAPPER.reader(DeserializationFeature.USE_LONG_FOR_INTS); + + Object ob = r.forType(Object.class).readValue("42"); + assertEquals(Long.class, ob.getClass()); + assertEquals(Long.valueOf(42L), ob); + + Number n = r.forType(Number.class).readValue("42"); + assertEquals(Long.class, n.getClass()); + assertEquals(Long.valueOf(42L), n); + + // and one more: should get proper node as well + JsonNode node = r.readTree("42"); + if (!node.isLong()) { + fail("Expected LongNode, got: "+node.getClass().getName()); + } + assertEquals(42, node.asInt()); } } diff --git a/src/test/java/com/fasterxml/jackson/databind/node/TestNumberNodes.java b/src/test/java/com/fasterxml/jackson/databind/node/TestNumberNodes.java index ce126604a8..f5b71c4ecf 100644 --- a/src/test/java/com/fasterxml/jackson/databind/node/TestNumberNodes.java +++ b/src/test/java/com/fasterxml/jackson/databind/node/TestNumberNodes.java @@ -293,9 +293,12 @@ public void testCanonicalNumbers() throws Exception n = f.numberNode(1L + Integer.MAX_VALUE); assertFalse(n.isInt()); assertTrue(n.isLong()); + + /* 19-May-2015, tatu: Actually, no, coercion should not happen by default. + * But it should be possible to change it if necessary. + */ // but "too small" number will be 'int'... n = f.numberNode(123L); - assertTrue(n.isInt()); - assertFalse(n.isLong()); + assertTrue(n.isLong()); } }