diff --git a/release-notes/CREDITS b/release-notes/CREDITS index 811770da43..291f4be7ff 100644 --- a/release-notes/CREDITS +++ b/release-notes/CREDITS @@ -623,3 +623,8 @@ Lyor Goldstein (lgoldstein@github) * Reported #1544: `EnumMapDeserializer` assumes a pure `EnumMap` and does not support derived classes (2.9.0) + +Harleen Sahni (harleensahni@github) + * Reported #403: Make FAIL_ON_NULL_FOR_PRIMITIVES apply to primitive arrays and other + types that wrap primitives + (2.9.0) diff --git a/release-notes/VERSION b/release-notes/VERSION index 6bdaf900ad..5a3dce8f4b 100644 --- a/release-notes/VERSION +++ b/release-notes/VERSION @@ -13,6 +13,8 @@ Project: jackson-databind (reported by Starkom@github) #357: StackOverflowError with contentConverter that returns array type (reported by Florian S) +#403: Make FAIL_ON_NULL_FOR_PRIMITIVES apply to primitive arrays and other types that wrap primitives + (reported by Harleen S) #476: Allow "Serialize as POJO" using `@JsonFormat(shape=Shape.OBJECT)` class annotation #507: Support for default `@JsonView` for a class (suggested by Mark W) 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 5e713f8bad..8c175d7230 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 @@ -190,7 +190,7 @@ protected Object _coerceTextualNull(DeserializationContext ctxt) throws JsonMapp { if (_primitive && ctxt.isEnabled(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES)) { ctxt.reportInputMismatch(this, - "Can not map String `null` into type %s (set DeserializationConfig.DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES to 'false' to allow)", + "Can not map String \"null\" into type %s (set DeserializationConfig.DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES to 'false' to allow)", handledType().toString()); } return _nullValue; diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/std/PrimitiveArrayDeserializers.java b/src/main/java/com/fasterxml/jackson/databind/deser/std/PrimitiveArrayDeserializers.java index 6c817cefb8..c3858d853e 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/std/PrimitiveArrayDeserializers.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/std/PrimitiveArrayDeserializers.java @@ -299,9 +299,13 @@ public char[] deserialize(JsonParser p, DeserializationContext ctxt) throws IOEx String str; if (t == JsonToken.VALUE_STRING) { str = p.getText(); - } else if ((t == JsonToken.VALUE_NULL) && (_nuller != null)) { - _nuller.getNullValue(ctxt); - continue; + } else if (t == JsonToken.VALUE_NULL) { + if (_nuller != null) { + _nuller.getNullValue(ctxt); + continue; + } + _verifyPrimitiveNull(ctxt); + str = "\0"; } else { CharSequence cs = (CharSequence) ctxt.handleUnexpectedToken(Character.TYPE, p); str = cs.toString(); @@ -380,7 +384,7 @@ protected boolean[] _constructEmpty() { @Override public boolean[] deserialize(JsonParser p, DeserializationContext ctxt) - throws IOException, JsonProcessingException + throws IOException { if (!p.isExpectedStartArrayToken()) { return handleNonArray(p, ctxt); @@ -402,6 +406,7 @@ public boolean[] deserialize(JsonParser p, DeserializationContext ctxt) _nuller.getNullValue(ctxt); continue; } + _verifyPrimitiveNull(ctxt); value = false; } else { value = _parseBooleanPrimitive(p, ctxt); @@ -510,10 +515,10 @@ public byte[] deserialize(JsonParser p, DeserializationContext ctxt) throws IOEx _nuller.getNullValue(ctxt); continue; } + _verifyPrimitiveNull(ctxt); value = (byte) 0; } else { - Number n = (Number) ctxt.handleUnexpectedToken(_valueClass.getComponentType(), p); - value = n.byteValue(); + value = _parseBytePrimitive(p, ctxt); } } if (ix >= chunk.length) { @@ -544,6 +549,7 @@ protected byte[] handleSingleElementUnwrapped(JsonParser p, _nuller.getNullValue(ctxt); return (byte[]) getEmptyValue(ctxt); } + _verifyPrimitiveNull(ctxt); return null; } Number n = (Number) ctxt.handleUnexpectedToken(_valueClass.getComponentType(), p); @@ -603,6 +609,7 @@ public short[] deserialize(JsonParser p, DeserializationContext ctxt) throws IOE _nuller.getNullValue(ctxt); continue; } + _verifyPrimitiveNull(ctxt); value = (short) 0; } else { value = _parseShortPrimitive(p, ctxt); @@ -680,6 +687,7 @@ public int[] deserialize(JsonParser p, DeserializationContext ctxt) throws IOExc _nuller.getNullValue(ctxt); continue; } + _verifyPrimitiveNull(ctxt); value = 0; } else { value = _parseIntPrimitive(p, ctxt); @@ -757,6 +765,7 @@ public long[] deserialize(JsonParser p, DeserializationContext ctxt) throws IOEx _nuller.getNullValue(ctxt); continue; } + _verifyPrimitiveNull(ctxt); value = 0L; } else { value = _parseLongPrimitive(p, ctxt); 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 f694b31ce2..7207a5f717 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 @@ -142,7 +142,10 @@ protected final boolean _parseBooleanPrimitive(JsonParser p, DeserializationCont JsonToken t = p.getCurrentToken(); if (t == JsonToken.VALUE_TRUE) return true; if (t == JsonToken.VALUE_FALSE) return false; - if (t == JsonToken.VALUE_NULL) return false; + if (t == JsonToken.VALUE_NULL) { + _verifyPrimitiveNull(ctxt); + return false; + } // should accept ints too, (0 == false, otherwise true) if (t == JsonToken.VALUE_NUMBER_INT) { @@ -155,10 +158,11 @@ protected final boolean _parseBooleanPrimitive(JsonParser p, DeserializationCont if ("true".equals(text) || "True".equals(text)) { return true; } - if ("false".equals(text) || "False".equals(text) || text.length() == 0) { + if ("false".equals(text) || "False".equals(text)) { return false; } - if (_hasTextualNull(text)) { + if ((text.length() == 0) || _hasTextualNull(text)) { + _verifyPrimitiveNullCoercion(ctxt, text); return false; } Boolean b = (Boolean) ctxt.handleWeirdStringValue(_valueClass, text, @@ -192,6 +196,19 @@ protected boolean _parseBooleanFromInt(JsonParser p, DeserializationContext ctxt return !"0".equals(p.getText()); } + protected final byte _parseBytePrimitive(JsonParser p, DeserializationContext ctxt) + throws IOException + { + int value = _parseIntPrimitive(p, ctxt); + // So far so good: but does it fit? + if (_byteOverflow(value)) { + Number v = (Number) ctxt.handleWeirdStringValue(_valueClass, String.valueOf(value), + "overflow, value can not be represented as 8-bit value"); + return (v == null) ? (byte) 0 : v.byteValue(); + } + return (byte) value; + } + protected final short _parseShortPrimitive(JsonParser p, DeserializationContext ctxt) throws IOException { @@ -214,7 +231,8 @@ protected final int _parseIntPrimitive(JsonParser p, DeserializationContext ctxt JsonToken t = p.getCurrentToken(); if (t == JsonToken.VALUE_STRING) { // let's do implicit re-parse String text = p.getText().trim(); - if (_hasTextualNull(text)) { + if ((text.length() == 0) || _hasTextualNull(text)) { + _verifyPrimitiveNullCoercion(ctxt, text); return 0; } try { @@ -246,6 +264,7 @@ protected final int _parseIntPrimitive(JsonParser p, DeserializationContext ctxt return p.getValueAsInt(); } if (t == JsonToken.VALUE_NULL) { + _verifyPrimitiveNull(ctxt); return 0; } if (t == JsonToken.START_ARRAY && ctxt.isEnabled(DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS)) { @@ -275,6 +294,7 @@ protected final long _parseLongPrimitive(JsonParser p, DeserializationContext ct case JsonTokenId.ID_STRING: String text = p.getText().trim(); if (text.length() == 0 || _hasTextualNull(text)) { + _verifyPrimitiveNullCoercion(ctxt, text); return 0L; } try { @@ -286,6 +306,7 @@ protected final long _parseLongPrimitive(JsonParser p, DeserializationContext ct return (v == null) ? 0 : v.longValue(); } case JsonTokenId.ID_NULL: + _verifyPrimitiveNull(ctxt); return 0L; case JsonTokenId.ID_START_ARRAY: if (ctxt.isEnabled(DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS)) { @@ -313,6 +334,7 @@ protected final float _parseFloatPrimitive(JsonParser p, DeserializationContext if (t == JsonToken.VALUE_STRING) { String text = p.getText().trim(); if (text.length() == 0 || _hasTextualNull(text)) { + _verifyPrimitiveNullCoercion(ctxt, text); return 0.0f; } switch (text.charAt(0)) { @@ -338,6 +360,7 @@ protected final float _parseFloatPrimitive(JsonParser p, DeserializationContext return (v == null) ? 0 : v.floatValue(); } if (t == JsonToken.VALUE_NULL) { + _verifyPrimitiveNull(ctxt); return 0.0f; } if (t == JsonToken.START_ARRAY && ctxt.isEnabled(DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS)) { @@ -366,6 +389,7 @@ protected final double _parseDoublePrimitive(JsonParser p, DeserializationContex if (t == JsonToken.VALUE_STRING) { String text = p.getText().trim(); if (text.length() == 0 || _hasTextualNull(text)) { + _verifyPrimitiveNullCoercion(ctxt, text); return 0.0; } switch (text.charAt(0)) { @@ -393,6 +417,7 @@ protected final double _parseDoublePrimitive(JsonParser p, DeserializationContex return (v == null) ? 0 : v.doubleValue(); } if (t == JsonToken.VALUE_NULL) { + _verifyPrimitiveNull(ctxt); return 0.0; } // [databind#381] @@ -859,6 +884,25 @@ protected void _failDoubleToIntCoercion(JsonParser p, DeserializationContext ctx p.getValueAsString(), type); } + protected final void _verifyPrimitiveNull(DeserializationContext ctxt) throws IOException + { + if (ctxt.isEnabled(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES)) { + ctxt.reportInputMismatch(this, + "Can not map `null` into primitive contents of type %s (set DeserializationConfig.DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES to 'false' to allow)", + handledType().getSimpleName()); + } + } + + protected final void _verifyPrimitiveNullCoercion(DeserializationContext ctxt, String str) throws IOException + { + if (ctxt.isEnabled(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES)) { + ctxt.reportInputMismatch(this, + "Can not map String \"%s\" into primitive contents of type %s (set DeserializationConfig.DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES to 'false' to allow)", + str, + handledType().getSimpleName()); + } + } + /* /********************************************************** /* Helper methods, other diff --git a/src/test/java/com/fasterxml/jackson/databind/deser/jdk/JDKScalarsTest.java b/src/test/java/com/fasterxml/jackson/databind/deser/jdk/JDKScalarsTest.java index bbb9885e76..ef32d38c46 100644 --- a/src/test/java/com/fasterxml/jackson/databind/deser/jdk/JDKScalarsTest.java +++ b/src/test/java/com/fasterxml/jackson/databind/deser/jdk/JDKScalarsTest.java @@ -1,6 +1,7 @@ package com.fasterxml.jackson.databind.deser.jdk; import java.io.*; +import java.lang.reflect.Array; import java.math.BigDecimal; import java.math.BigInteger; @@ -1062,12 +1063,73 @@ public void testEmptyStringForPrimitives() throws IOException assertEquals(0.0, bean.doubleValue); } + // for [databind#403] + public void testEmptyStringFailForPrimitives() throws IOException + { + final ObjectReader reader = MAPPER + .readerFor(PrimitivesBean.class) + .with(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES); + + // boolean + try { + reader.readValue("{\"booleanValue\":\"\"}"); + fail("Expected failure for boolean + empty String"); + } catch (JsonMappingException e) { + verifyException(e, "Can not map empty String (\"\") into type boolean"); + } + // byte/char/short/int/long + try { + reader.readValue("{\"byteValue\":\"\"}"); + fail("Expected failure for byte + empty String"); + } catch (JsonMappingException e) { + verifyException(e, "Can not map empty String (\"\") into type byte"); + } + try { + reader.readValue("{\"charValue\":\"\"}"); + fail("Expected failure for char + empty String"); + } catch (JsonMappingException e) { + verifyException(e, "Can not map empty String (\"\") into type char"); + } + try { + reader.readValue("{\"shortValue\":\"\"}"); + fail("Expected failure for short + empty String"); + } catch (JsonMappingException e) { + verifyException(e, "Can not map empty String (\"\") into type short"); + } + try { + reader.readValue("{\"intValue\":\"\"}"); + fail("Expected failure for int + empty String"); + } catch (JsonMappingException e) { + verifyException(e, "Can not map empty String (\"\") into type int"); + } + try { + reader.readValue("{\"longValue\":\"\"}"); + fail("Expected failure for long + empty String"); + } catch (JsonMappingException e) { + verifyException(e, "Can not map empty String (\"\") into type long"); + } + + // float/double + try { + reader.readValue("{\"floatValue\":\"\"}"); + fail("Expected failure for float + empty String"); + } catch (JsonMappingException e) { + verifyException(e, "Can not map empty String (\"\") into type float"); + } + try { + reader.readValue("{\"doubleValue\":\"\"}"); + fail("Expected failure for double + empty String"); + } catch (JsonMappingException e) { + verifyException(e, "Can not map empty String (\"\") into type double"); + } + } + /* /********************************************************** /* Null handling for scalars in POJO /********************************************************** */ - + public void testNullForPrimitives() throws IOException { // by default, ok to rely on defaults @@ -1085,7 +1147,7 @@ public void testNullForPrimitives() throws IOException assertEquals((byte) 0, bean.byteValue); assertEquals(0L, bean.longValue); assertEquals(0.0f, bean.floatValue); - + // but not when enabled final ObjectReader reader = MAPPER .readerFor(PrimitivesBean.class) @@ -1143,4 +1205,54 @@ public void testNullForPrimitives() throws IOException verifyException(e, "Can not map `null` into type double"); } } + + public void testNullForPrimitiveArrays() throws IOException + { + _testNullForPrimitiveArrays(boolean[].class, Boolean.FALSE); + _testNullForPrimitiveArrays(byte[].class, Byte.valueOf((byte) 0)); + _testNullForPrimitiveArrays(char[].class, Character.valueOf((char) 0), false); + _testNullForPrimitiveArrays(short[].class, Short.valueOf((short)0)); + _testNullForPrimitiveArrays(int[].class, Integer.valueOf(0)); + _testNullForPrimitiveArrays(long[].class, Long.valueOf(0L)); + _testNullForPrimitiveArrays(float[].class, Float.valueOf(0f)); + _testNullForPrimitiveArrays(double[].class, Double.valueOf(0d)); + } + + private void _testNullForPrimitiveArrays(Class cls, Object defValue) throws IOException { + _testNullForPrimitiveArrays(cls, defValue, true); + } + + private void _testNullForPrimitiveArrays(Class cls, Object defValue, + boolean testEmptyString) throws IOException + { + final String EMPTY_STRING_JSON = "[ \"\" ]"; + final String JSON_WITH_NULL = "[ null ]"; + final String SIMPLE_NAME = cls.getSimpleName(); + final ObjectReader readerCoerceOk = MAPPER.readerFor(cls); + final ObjectReader readerNoCoerce = readerCoerceOk + .with(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES); + + Object ob = readerCoerceOk.forType(cls).readValue(JSON_WITH_NULL); + assertEquals(1, Array.getLength(ob)); + assertEquals(defValue, Array.get(ob, 0)); + try { + readerNoCoerce.readValue(JSON_WITH_NULL); + fail("Should not pass"); + } catch (JsonMappingException e) { + verifyException(e, "Can not map `null` into primitive contents of type "+SIMPLE_NAME); + } + + if (testEmptyString) { + ob = readerCoerceOk.forType(cls).readValue(EMPTY_STRING_JSON); + assertEquals(1, Array.getLength(ob)); + assertEquals(defValue, Array.get(ob, 0)); + + try { + readerNoCoerce.readValue(EMPTY_STRING_JSON); + fail("Should not pass"); + } catch (JsonMappingException e) { + verifyException(e, "Can not map String \"\" into primitive contents of type "+SIMPLE_NAME); + } + } + } }