From 3749d7c8e6a3aef3f8a0888ea9751c7efb063d08 Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Mon, 27 Mar 2017 20:45:18 -0700 Subject: [PATCH] Fixed #1480 --- release-notes/VERSION | 2 + .../databind/ser/std/BooleanSerializer.java | 95 +++++++++++++++++-- .../databind/ser/std/EnumSerializer.java | 17 ++-- .../jackson/databind/BaseMapTest.java | 12 +-- .../deser/TestGenericCollectionDeser.java | 27 +++--- .../databind/format/BooleanFormatTest.java | 60 ++++++++++++ .../jsontype/TestPropertyTypeInfo.java | 31 +++--- 7 files changed, 192 insertions(+), 52 deletions(-) create mode 100644 src/test/java/com/fasterxml/jackson/databind/format/BooleanFormatTest.java diff --git a/release-notes/VERSION b/release-notes/VERSION index 05974262b7..8d26209d75 100644 --- a/release-notes/VERSION +++ b/release-notes/VERSION @@ -60,6 +60,8 @@ Project: jackson-databind (suggested by PawelJagus@github) #1454: Support `@JsonFormat.lenient` for `java.util.Date`, `java.util.Calendar` #1474: Replace use of `Class.newInstance()` (deprecated in Java 9) with call via Constructor +#1480: Add support for serializing `boolean`/`Boolean` as number (0 or 1) + (suggested by jwilmoth@github) #1520: Case insensitive enum deserialization feature. (contributed by Ana-Eliza B) #1544: EnumMapDeserializer assumes a pure EnumMap and does not support EnumMap derived classes diff --git a/src/main/java/com/fasterxml/jackson/databind/ser/std/BooleanSerializer.java b/src/main/java/com/fasterxml/jackson/databind/ser/std/BooleanSerializer.java index 6b27ef6111..9aaff5fac8 100644 --- a/src/main/java/com/fasterxml/jackson/databind/ser/std/BooleanSerializer.java +++ b/src/main/java/com/fasterxml/jackson/databind/ser/std/BooleanSerializer.java @@ -3,15 +3,19 @@ import java.io.IOException; import java.lang.reflect.Type; +import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.core.JsonGenerator; - +import com.fasterxml.jackson.core.JsonParser.NumberType; +import com.fasterxml.jackson.databind.BeanProperty; import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.JsonSerializer; import com.fasterxml.jackson.databind.SerializerProvider; import com.fasterxml.jackson.databind.annotation.JacksonStdImpl; import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonFormatVisitorWrapper; import com.fasterxml.jackson.databind.jsontype.TypeSerializer; +import com.fasterxml.jackson.databind.ser.ContextualSerializer; /** * Serializer used for primitive boolean, as well as java.util.Boolean @@ -24,6 +28,7 @@ public final class BooleanSerializer //In 2.9, removed use of intermediate type `NonTypedScalarSerializerBase` extends StdScalarSerializer + implements ContextualSerializer { private static final long serialVersionUID = 1L; @@ -38,26 +43,102 @@ public BooleanSerializer(boolean forPrimitive) { _forPrimitive = forPrimitive; } + @Override + public JsonSerializer createContextual(SerializerProvider serializers, + BeanProperty property) throws JsonMappingException + { + JsonFormat.Value format = findFormatOverrides(serializers, + property, Boolean.class); + if (format != null) { + JsonFormat.Shape shape = format.getShape(); + if (shape.isNumeric()) { + return new AsNumber(_forPrimitive); + } + } + return this; + } + @Override public void serialize(Object value, JsonGenerator g, SerializerProvider provider) throws IOException { - g.writeBoolean(((Boolean) value).booleanValue()); + g.writeBoolean(Boolean.TRUE.equals(value)); } @Override public final void serializeWithType(Object value, JsonGenerator g, SerializerProvider provider, TypeSerializer typeSer) throws IOException { - g.writeBoolean(((Boolean) value).booleanValue()); + g.writeBoolean(Boolean.TRUE.equals(value)); } - + @Override public JsonNode getSchema(SerializerProvider provider, Type typeHint) { return createSchemaNode("boolean", !_forPrimitive); } - + @Override - public void acceptJsonFormatVisitor(JsonFormatVisitorWrapper visitor, JavaType typeHint) throws JsonMappingException - { + public void acceptJsonFormatVisitor(JsonFormatVisitorWrapper visitor, JavaType typeHint) throws JsonMappingException { visitor.expectBooleanFormat(typeHint); } + + /** + * Alternate implementation that is used when values are to be serialized + * as numbers 0 (false) or 1 (true). + * + * @since 2.9 + */ + final static class AsNumber + extends StdScalarSerializer + implements ContextualSerializer + { + private static final long serialVersionUID = 1L; + + /** + * Whether type serialized is primitive (boolean) or wrapper + * (java.lang.Boolean); if true, former, if false, latter. + */ + protected final boolean _forPrimitive; + + public AsNumber(boolean forPrimitive) { + super(forPrimitive ? Boolean.TYPE : Boolean.class, false); + _forPrimitive = forPrimitive; + } + + @Override + public void serialize(Object value, JsonGenerator g, SerializerProvider provider) throws IOException { + g.writeNumber((Boolean.FALSE.equals(value)) ? 0 : 1); + } + + @Override + public final void serializeWithType(Object value, JsonGenerator g, SerializerProvider provider, + TypeSerializer typeSer) throws IOException + { + // 27-Mar-2017, tatu: Actually here we CAN NOT serialize as number without type, + // since with natural types that would map to number, not boolean. So choice + // comes to between either add type id, or serialize as boolean. Choose + // latter at this point + g.writeBoolean(Boolean.TRUE.equals(value)); + } + + @Override + public void acceptJsonFormatVisitor(JsonFormatVisitorWrapper visitor, JavaType typeHint) throws JsonMappingException { + // 27-Mar-2017, tatu: As usual, bit tricky but... seems like we should call + // visitor for actual representation + visitIntFormat(visitor, typeHint, NumberType.INT); + } + + @Override + public JsonSerializer createContextual(SerializerProvider serializers, + BeanProperty property) throws JsonMappingException + { + JsonFormat.Value format = findFormatOverrides(serializers, + property, Boolean.class); + if (format != null) { + JsonFormat.Shape shape = format.getShape(); + if (!shape.isNumeric()) { + return new BooleanSerializer(_forPrimitive); + } + } + return this; + } +} } diff --git a/src/main/java/com/fasterxml/jackson/databind/ser/std/EnumSerializer.java b/src/main/java/com/fasterxml/jackson/databind/ser/std/EnumSerializer.java index 9df4a3adc9..3a048b8284 100644 --- a/src/main/java/com/fasterxml/jackson/databind/ser/std/EnumSerializer.java +++ b/src/main/java/com/fasterxml/jackson/databind/ser/std/EnumSerializer.java @@ -88,15 +88,14 @@ public static EnumSerializer construct(Class enumClass, SerializationConfig c public JsonSerializer createContextual(SerializerProvider serializers, BeanProperty property) throws JsonMappingException { - if (property != null) { - JsonFormat.Value format = findFormatOverrides(serializers, - property, handledType()); - if (format != null) { - Boolean serializeAsIndex = _isShapeWrittenUsingIndex(property.getType().getRawClass(), - format, false, _serializeAsIndex); - if (serializeAsIndex != _serializeAsIndex) { - return new EnumSerializer(_values, serializeAsIndex); - } + JsonFormat.Value format = findFormatOverrides(serializers, + property, handledType()); + if (format != null) { + Class type = handledType(); + Boolean serializeAsIndex = _isShapeWrittenUsingIndex(type, + format, false, _serializeAsIndex); + if (serializeAsIndex != _serializeAsIndex) { + return new EnumSerializer(_values, serializeAsIndex); } } return this; diff --git a/src/test/java/com/fasterxml/jackson/databind/BaseMapTest.java b/src/test/java/com/fasterxml/jackson/databind/BaseMapTest.java index d19e5f5c40..01eb2d76cc 100644 --- a/src/test/java/com/fasterxml/jackson/databind/BaseMapTest.java +++ b/src/test/java/com/fasterxml/jackson/databind/BaseMapTest.java @@ -6,11 +6,9 @@ import static org.junit.Assert.*; import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonValue; -import com.fasterxml.jackson.core.FormatSchema; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.JsonProcessingException; + +import com.fasterxml.jackson.core.*; + import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.deser.std.StdScalarDeserializer; import com.fasterxml.jackson.databind.ser.std.StdScalarSerializer; @@ -41,10 +39,8 @@ public String getSchemaType() { protected static class BooleanWrapper { public Boolean b; - @JsonCreator + public BooleanWrapper() { } public BooleanWrapper(Boolean value) { b = value; } - - @JsonValue public Boolean value() { return b; } } protected static class IntWrapper { diff --git a/src/test/java/com/fasterxml/jackson/databind/deser/TestGenericCollectionDeser.java b/src/test/java/com/fasterxml/jackson/databind/deser/TestGenericCollectionDeser.java index f99bbb2daa..a87670e276 100644 --- a/src/test/java/com/fasterxml/jackson/databind/deser/TestGenericCollectionDeser.java +++ b/src/test/java/com/fasterxml/jackson/databind/deser/TestGenericCollectionDeser.java @@ -2,6 +2,8 @@ import java.util.*; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; import com.fasterxml.jackson.databind.*; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; @@ -9,12 +11,6 @@ public class TestGenericCollectionDeser extends BaseMapTest { - /* - /********************************************************** - /* Test classes, enums - /********************************************************** - */ - static class ListSubClass extends ArrayList { } /** @@ -24,15 +20,18 @@ static class ListSubClass extends ArrayList { } @JsonDeserialize(contentAs=StringWrapper.class) static class AnnotatedStringList extends ArrayList { } - @JsonDeserialize(contentAs=BooleanWrapper.class) + @JsonDeserialize(contentAs=BooleanElement.class) static class AnnotatedBooleanList extends ArrayList { } - /* - /********************************************************** - /* Test methods - /********************************************************** - */ + protected static class BooleanElement { + public Boolean b; + @JsonCreator + public BooleanElement(Boolean value) { b = value; } + + @JsonValue public Boolean value() { return b; } + } + /* /********************************************************** /* Tests for sub-classing @@ -76,7 +75,7 @@ public void testAnnotatedBooleanList() throws Exception AnnotatedBooleanList result = mapper.readValue("[ false ]", AnnotatedBooleanList.class); assertEquals(1, result.size()); Object ob = result.get(0); - assertEquals(BooleanWrapper.class, ob.getClass()); - assertFalse(((BooleanWrapper) ob).b); + assertEquals(BooleanElement.class, ob.getClass()); + assertFalse(((BooleanElement) ob).b); } } diff --git a/src/test/java/com/fasterxml/jackson/databind/format/BooleanFormatTest.java b/src/test/java/com/fasterxml/jackson/databind/format/BooleanFormatTest.java new file mode 100644 index 0000000000..5d2434654a --- /dev/null +++ b/src/test/java/com/fasterxml/jackson/databind/format/BooleanFormatTest.java @@ -0,0 +1,60 @@ +package com.fasterxml.jackson.databind.format; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.databind.BaseMapTest; +import com.fasterxml.jackson.databind.ObjectMapper; + +// [databind#1480] +public class BooleanFormatTest extends BaseMapTest +{ + @JsonPropertyOrder({ "b1", "b2", "b3" }) + static class BeanWithBoolean + { + @JsonFormat(shape=JsonFormat.Shape.NUMBER) + public boolean b1; + + @JsonFormat(shape=JsonFormat.Shape.NUMBER) + public Boolean b2; + + public boolean b3; + + public BeanWithBoolean() { } + public BeanWithBoolean(boolean b1, Boolean b2, boolean b3) { + this.b1 = b1; + this.b2 = b2; + this.b3 = b3; + } + } + + static class AltBoolean extends BooleanWrapper + { + public AltBoolean() { } + public AltBoolean(Boolean b) { super(b); } + } + + /* + /********************************************************** + /* Test methods + /********************************************************** + */ + + private final static ObjectMapper MAPPER = new ObjectMapper(); + + public void testShapeViaDefaults() throws Exception + { + assertEquals(aposToQuotes("{'b':true}"), + MAPPER.writeValueAsString(new BooleanWrapper(true))); + ObjectMapper m = new ObjectMapper(); + m.configOverride(Boolean.class) + .setFormat(JsonFormat.Value.forShape(JsonFormat.Shape.NUMBER)); + assertEquals(aposToQuotes("{'b':1}"), + m.writeValueAsString(new BooleanWrapper(true))); + } + + public void testShapeOnProperty() throws Exception + { + assertEquals(aposToQuotes("{'b1':1,'b2':0,'b3':true}"), + MAPPER.writeValueAsString(new BeanWithBoolean(true, false, true))); + } +} diff --git a/src/test/java/com/fasterxml/jackson/databind/jsontype/TestPropertyTypeInfo.java b/src/test/java/com/fasterxml/jackson/databind/jsontype/TestPropertyTypeInfo.java index 484f6cc9d8..5f043a366d 100644 --- a/src/test/java/com/fasterxml/jackson/databind/jsontype/TestPropertyTypeInfo.java +++ b/src/test/java/com/fasterxml/jackson/databind/jsontype/TestPropertyTypeInfo.java @@ -14,11 +14,14 @@ @SuppressWarnings("serial") public class TestPropertyTypeInfo extends BaseMapTest { - /* - /********************************************************** - /* Helper types - /********************************************************** - */ + protected static class BooleanValue { + public Boolean b; + + @JsonCreator + public BooleanValue(Boolean value) { b = value; } + + @JsonValue public Boolean value() { return b; } + } static class FieldWrapperBean { @@ -120,7 +123,7 @@ public void testSimpleListMethod() throws Exception { ObjectMapper mapper = new ObjectMapper(); MethodWrapperBeanList list = new MethodWrapperBeanList(); - list.add(new MethodWrapperBean(new BooleanWrapper(true))); + list.add(new MethodWrapperBean(new BooleanValue(true))); list.add(new MethodWrapperBean(new StringWrapper("x"))); list.add(new MethodWrapperBean(new OtherBean())); String json = mapper.writeValueAsString(list); @@ -128,8 +131,8 @@ public void testSimpleListMethod() throws Exception assertNotNull(result); assertEquals(3, result.size()); MethodWrapperBean bean = result.get(0); - assertEquals(BooleanWrapper.class, bean.value.getClass()); - assertEquals(((BooleanWrapper) bean.value).b, Boolean.TRUE); + assertEquals(BooleanValue.class, bean.value.getClass()); + assertEquals(((BooleanValue) bean.value).b, Boolean.TRUE); bean = result.get(1); assertEquals(StringWrapper.class, bean.value.getClass()); assertEquals(((StringWrapper) bean.value).str, "x"); @@ -141,15 +144,15 @@ public void testSimpleArrayField() throws Exception { ObjectMapper mapper = new ObjectMapper(); FieldWrapperBeanArray array = new FieldWrapperBeanArray(new - FieldWrapperBean[] { new FieldWrapperBean(new BooleanWrapper(true)) }); + FieldWrapperBean[] { new FieldWrapperBean(new BooleanValue(true)) }); String json = mapper.writeValueAsString(array); FieldWrapperBeanArray result = mapper.readValue(json, FieldWrapperBeanArray.class); assertNotNull(result); FieldWrapperBean[] beans = result.beans; assertEquals(1, beans.length); FieldWrapperBean bean = beans[0]; - assertEquals(BooleanWrapper.class, bean.value.getClass()); - assertEquals(((BooleanWrapper) bean.value).b, Boolean.TRUE); + assertEquals(BooleanValue.class, bean.value.getClass()); + assertEquals(((BooleanValue) bean.value).b, Boolean.TRUE); } public void testSimpleArrayMethod() throws Exception @@ -187,7 +190,7 @@ public void testSimpleMapMethod() throws Exception { ObjectMapper mapper = new ObjectMapper(); MethodWrapperBeanMap map = new MethodWrapperBeanMap(); - map.put("xyz", new MethodWrapperBean(new BooleanWrapper(true))); + map.put("xyz", new MethodWrapperBean(new BooleanValue(true))); String json = mapper.writeValueAsString(map); MethodWrapperBeanMap result = mapper.readValue(json, MethodWrapperBeanMap.class); assertNotNull(result); @@ -195,7 +198,7 @@ public void testSimpleMapMethod() throws Exception MethodWrapperBean bean = result.get("xyz"); assertNotNull(bean); Object ob = bean.value; - assertEquals(BooleanWrapper.class, ob.getClass()); - assertEquals(((BooleanWrapper) ob).b, Boolean.TRUE); + assertEquals(BooleanValue.class, ob.getClass()); + assertEquals(((BooleanValue) ob).b, Boolean.TRUE); } }