diff --git a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/annotation/AvroDecimal.java b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/annotation/AvroDecimal.java new file mode 100644 index 000000000..5205db285 --- /dev/null +++ b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/annotation/AvroDecimal.java @@ -0,0 +1,35 @@ +package com.fasterxml.jackson.dataformat.avro.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * When generate logical types is enabled, annotation instructs the + * {@link com.fasterxml.jackson.dataformat.avro.schema.AvroSchemaGenerator AvroSchemaGenerator} + * to declare the annotated property's logical type as "decimal" ({@link org.apache.avro.LogicalTypes.Decimal}). + * By default, the Avro type is "bytes" ({@link org.apache.avro.Schema.Type#BYTES}), unless the field is also + * annotated with {@link com.fasterxml.jackson.dataformat.avro.AvroFixedSize}, in which case the Avro type + * will be "fixed" ({@link org.apache.avro.Schema.Type#FIXED}). + *

+ * This annotation is only used during Avro schema generation and does not affect data serialization + * or deserialization. + * + * @since 2.19 + */ +@Target({ElementType.ANNOTATION_TYPE, ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface AvroDecimal { + + /** + * Maximum precision of decimals stored in this type. + */ + int precision(); + + /** + * Scale must be zero or a positive integer less than or equal to the precision. + */ + int scale(); + +} diff --git a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/deser/AvroParserImpl.java b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/deser/AvroParserImpl.java index d54893503..a0c3393dc 100644 --- a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/deser/AvroParserImpl.java +++ b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/deser/AvroParserImpl.java @@ -581,6 +581,38 @@ public long getRemainingElements() public abstract int decodeIndex() throws IOException; public abstract int decodeEnum() throws IOException; + /* + /********************************************************** + /* Methods for AvroReadContext implementations: decimals + /********************************************************** + */ + + // @since 2.19 + public JsonToken decodeBytesDecimal(int scale) throws IOException { + decodeBytes(); + _numberBigDecimal = new BigDecimal(new BigInteger(_binaryValue), scale); + _numTypesValid = NR_BIGDECIMAL; + return JsonToken.VALUE_NUMBER_FLOAT; + } + + // @since 2.19 + public void skipBytesDecimal() throws IOException { + skipBytes(); + } + + // @since 2.19 + public JsonToken decodeFixedDecimal(int scale, int size) throws IOException { + decodeFixed(size); + _numberBigDecimal = new BigDecimal(new BigInteger(_binaryValue), scale); + _numTypesValid = NR_BIGDECIMAL; + return JsonToken.VALUE_NUMBER_FLOAT; + } + + // @since 2.19 + public void skipFixedDecimal(int size) throws IOException { + skipFixed(size); + } + /* /********************************************************** /* Methods for AvroReadContext impls, other diff --git a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/deser/AvroReaderFactory.java b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/deser/AvroReaderFactory.java index b14f0bbd4..818fb7278 100644 --- a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/deser/AvroReaderFactory.java +++ b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/deser/AvroReaderFactory.java @@ -3,6 +3,7 @@ import java.io.IOException; import java.util.*; +import org.apache.avro.LogicalTypes; import org.apache.avro.Schema; import com.fasterxml.jackson.dataformat.avro.deser.ScalarDecoder.*; @@ -56,12 +57,18 @@ public ScalarDecoder createScalarValueDecoder(Schema type) case BOOLEAN: return READER_BOOLEAN; case BYTES: + if (type.getLogicalType() instanceof LogicalTypes.Decimal) { + return new BytesDecimalReader(((LogicalTypes.Decimal) type.getLogicalType()).getScale()); + } return READER_BYTES; case DOUBLE: return READER_DOUBLE; case ENUM: return new EnumDecoder(AvroSchemaHelper.getFullName(type), type.getEnumSymbols()); case FIXED: + if (type.getLogicalType() instanceof LogicalTypes.Decimal) { + return new FixedDecimalReader(((LogicalTypes.Decimal) type.getLogicalType()).getScale(), type.getFixedSize()); + } return new FixedDecoder(type.getFixedSize(), AvroSchemaHelper.getFullName(type)); case FLOAT: return READER_FLOAT; diff --git a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/deser/ScalarDecoder.java b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/deser/ScalarDecoder.java index 9f41bb611..1c99c51bd 100644 --- a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/deser/ScalarDecoder.java +++ b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/deser/ScalarDecoder.java @@ -1,6 +1,7 @@ package com.fasterxml.jackson.dataformat.avro.deser; import java.io.IOException; +import java.math.BigDecimal; import java.util.List; import com.fasterxml.jackson.core.JsonToken; @@ -546,4 +547,106 @@ public void skipValue(AvroParserImpl parser) throws IOException { } } } + + /** + * @since 2.19 + */ + protected final static class FixedDecimalReader extends ScalarDecoder { + private final int _scale; + private final int _size; + + public FixedDecimalReader(int scale, int size) { + _scale = scale; + _size = size; + } + + @Override + public JsonToken decodeValue(AvroParserImpl parser) throws IOException { + return parser.decodeFixedDecimal(_scale, _size); + } + + @Override + protected void skipValue(AvroParserImpl parser) throws IOException { + parser.skipFixedDecimal(_size); + } + + @Override + public String getTypeId() { + return AvroSchemaHelper.getTypeId(BigDecimal.class); + } + + @Override + public AvroFieldReader asFieldReader(String name, boolean skipper) { + return new FR(name, skipper, getTypeId(), _scale, _size); + } + + private final static class FR extends AvroFieldReader { + private final int _scale; + private final int _size; + public FR(String name, boolean skipper, String typeId, int scale, int size) { + super(name, skipper, typeId); + _scale = scale; + _size = size; + } + + @Override + public JsonToken readValue(AvroReadContext parent, AvroParserImpl parser) throws IOException { + return parser.decodeFixedDecimal(_scale, _size); + } + + @Override + public void skipValue(AvroParserImpl parser) throws IOException { + parser.skipFixedDecimal(_size); + } + } + } + + /** + * @since 2.19 + */ + protected final static class BytesDecimalReader extends ScalarDecoder { + private final int _scale; + + public BytesDecimalReader(int scale) { + _scale = scale; + } + + @Override + public JsonToken decodeValue(AvroParserImpl parser) throws IOException { + return parser.decodeBytesDecimal(_scale); + } + + @Override + protected void skipValue(AvroParserImpl parser) throws IOException { + parser.skipBytesDecimal(); + } + + @Override + public String getTypeId() { + return AvroSchemaHelper.getTypeId(BigDecimal.class); + } + + @Override + public AvroFieldReader asFieldReader(String name, boolean skipper) { + return new FR(name, skipper, getTypeId(), _scale); + } + + private final static class FR extends AvroFieldReader { + private final int _scale; + public FR(String name, boolean skipper, String typeId, int scale) { + super(name, skipper, typeId); + _scale = scale; + } + + @Override + public JsonToken readValue(AvroReadContext parent, AvroParserImpl parser) throws IOException { + return parser.decodeBytesDecimal(_scale); + } + + @Override + public void skipValue(AvroParserImpl parser) throws IOException { + parser.skipFloat(); + } + } + } } diff --git a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/schema/RecordVisitor.java b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/schema/RecordVisitor.java index 0eead85e8..8aa8f9b32 100644 --- a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/schema/RecordVisitor.java +++ b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/schema/RecordVisitor.java @@ -4,6 +4,7 @@ import java.util.List; import java.util.Map; +import org.apache.avro.LogicalTypes; import org.apache.avro.Schema; import org.apache.avro.Schema.Type; import org.apache.avro.reflect.AvroMeta; @@ -15,6 +16,7 @@ import com.fasterxml.jackson.databind.jsontype.NamedType; import com.fasterxml.jackson.databind.ser.BeanPropertyWriter; import com.fasterxml.jackson.dataformat.avro.AvroFixedSize; +import com.fasterxml.jackson.dataformat.avro.annotation.AvroDecimal; import com.fasterxml.jackson.dataformat.avro.ser.CustomEncodingSerializer; public class RecordVisitor @@ -141,7 +143,7 @@ public void optionalProperty(String name, JsonFormatVisitable handler, protected Schema.Field schemaFieldForWriter(BeanProperty prop, boolean optional) throws JsonMappingException { - Schema writerSchema; + Schema writerSchema = null; // Check if schema for property is overridden AvroSchema schemaOverride = prop.getAnnotation(AvroSchema.class); if (schemaOverride != null) { @@ -151,16 +153,25 @@ protected Schema.Field schemaFieldForWriter(BeanProperty prop, boolean optional) AvroFixedSize fixedSize = prop.getAnnotation(AvroFixedSize.class); if (fixedSize != null) { writerSchema = Schema.createFixed(fixedSize.typeName(), null, fixedSize.typeNamespace(), fixedSize.size()); - } else { + } + if (_visitorWrapper.isLogicalTypesEnabled()) { + AvroDecimal avroDecimal = prop.getAnnotation(AvroDecimal.class); + if (avroDecimal != null) { + if (writerSchema == null) { + writerSchema = Schema.create(Type.BYTES); + } + writerSchema = LogicalTypes.decimal(avroDecimal.precision(), avroDecimal.scale()) + .addToSchema(writerSchema); + } + } + if (writerSchema == null) { JsonSerializer ser = null; // 23-Nov-2012, tatu: Ideally shouldn't need to do this but... if (prop instanceof BeanPropertyWriter) { BeanPropertyWriter bpw = (BeanPropertyWriter) prop; ser = bpw.getSerializer(); - /* - * 2-Mar-2017, bryan: AvroEncode annotation expects to have the schema used directly - */ + // 2-Mar-2017, bryan: AvroEncode annotation expects to have the schema used directly optional = optional && !(ser instanceof CustomEncodingSerializer); // Don't modify schema } final SerializerProvider prov = getProvider(); @@ -204,7 +215,7 @@ protected Schema.Field schemaFieldForWriter(BeanProperty prop, boolean optional) /** * A union schema with a default value must always have the schema branch corresponding to the default value first, or Avro will print a - * warning complaining that the default value is not compatible. If {@code schema} is a {@code Schema.Type.UNION} schema and + * warning complaining that the default value is not compatible. If {@code schema} is a {@link Type#UNION UNION} schema and * {@code defaultValue} is non-{@code null}, this finds the appropriate branch in the union and reorders the union so that it is first. * * @param schema diff --git a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/ser/NonBSGenericDatumWriter.java b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/ser/NonBSGenericDatumWriter.java index 827e8bc23..076fb1190 100644 --- a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/ser/NonBSGenericDatumWriter.java +++ b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/ser/NonBSGenericDatumWriter.java @@ -6,6 +6,7 @@ import java.nio.ByteBuffer; import java.util.ArrayList; +import org.apache.avro.Conversions.DecimalConversion; import org.apache.avro.Schema; import org.apache.avro.Schema.Type; import org.apache.avro.generic.GenericData; @@ -27,6 +28,9 @@ public class NonBSGenericDatumWriter private final static Class CLS_BIG_DECIMAL = BigDecimal.class; private final static Class CLS_BIG_INTEGER = BigInteger.class; + // @since 2.19 + private final static DecimalConversion BIG_DECIMAL_CONVERSION = new DecimalConversion(); + public NonBSGenericDatumWriter(Schema root) { super(root); } @@ -97,6 +101,11 @@ protected void write(Schema schema, Object datum, Encoder out) throws IOExceptio super.writeWithoutConversion(schema, ByteBuffer.wrap((byte[]) datum), out); return; } + if (datum.getClass() == CLS_BIG_DECIMAL) { + super.writeWithoutConversion(schema, BIG_DECIMAL_CONVERSION.toBytes( + (BigDecimal) datum, schema, schema.getLogicalType()), out); + return; + } break; case FIXED: // One more mismatch to fix @@ -111,6 +120,11 @@ protected void write(Schema schema, Object datum, Encoder out) throws IOExceptio super.writeWithoutConversion(schema, new GenericData.Fixed(schema, (byte[]) datum), out); return; } + if (datum.getClass() == CLS_BIG_DECIMAL) { + super.writeWithoutConversion(schema, BIG_DECIMAL_CONVERSION.toFixed( + (BigDecimal) datum, schema, schema.getLogicalType()), out); + return; + } break; default: diff --git a/avro/src/test/java/com/fasterxml/jackson/dataformat/avro/BigDecimalTest.java b/avro/src/test/java/com/fasterxml/jackson/dataformat/avro/BigDecimalTest.java deleted file mode 100644 index f8d796aeb..000000000 --- a/avro/src/test/java/com/fasterxml/jackson/dataformat/avro/BigDecimalTest.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.fasterxml.jackson.dataformat.avro; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; - -import java.math.BigDecimal; - -public class BigDecimalTest extends AvroTestBase -{ - public static class NamedAmount { - public final String name; - public final BigDecimal amount; - - @JsonCreator - public NamedAmount(@JsonProperty("name") String name, - @JsonProperty("amount") double amount) { - this.name = name; - this.amount = BigDecimal.valueOf(amount); - } - } - - public void testSerializeBigDecimal() throws Exception { - AvroMapper mapper = newMapper(); - AvroSchema schema = mapper.schemaFor(NamedAmount.class); - - byte[] bytes = mapper.writer(schema) - .writeValueAsBytes(new NamedAmount("peter", 42.0)); - - NamedAmount result = mapper.reader(schema).forType(NamedAmount.class).readValue(bytes); - - assertEquals("peter", result.name); - assertEquals(BigDecimal.valueOf(42.0), result.amount); - } -} diff --git a/avro/src/test/java/com/fasterxml/jackson/dataformat/avro/BigDecimal_schemaCreationTest.java b/avro/src/test/java/com/fasterxml/jackson/dataformat/avro/BigDecimal_schemaCreationTest.java new file mode 100644 index 000000000..48d8722e3 --- /dev/null +++ b/avro/src/test/java/com/fasterxml/jackson/dataformat/avro/BigDecimal_schemaCreationTest.java @@ -0,0 +1,102 @@ +package com.fasterxml.jackson.dataformat.avro; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.dataformat.avro.annotation.AvroDecimal; +import com.fasterxml.jackson.dataformat.avro.schema.AvroSchemaGenerator; +import org.apache.avro.LogicalTypes; +import org.apache.avro.Schema; +import org.junit.Test; + +import java.math.BigDecimal; + +import static org.assertj.core.api.Assertions.assertThat; + +public class BigDecimal_schemaCreationTest extends AvroTestBase { + private static final AvroMapper MAPPER = new AvroMapper(); + + static class BigDecimalWithAvroDecimalAnnotationWrapper { + @JsonProperty(required = true) // field is required to have simpler avro schema + @AvroDecimal(precision = 10, scale = 2) + public final BigDecimal bigDecimalValue; + + public BigDecimalWithAvroDecimalAnnotationWrapper(BigDecimal bigDecimalValue) { + this.bigDecimalValue = bigDecimalValue; + } + } + + @Test + public void testSchemaCreation_withLogicalTypesDisabled_onBigDecimalWithAvroDecimalAnnotation() throws JsonMappingException { + // GIVEN + AvroSchemaGenerator gen = new AvroSchemaGenerator() + .disableLogicalTypes(); + + // WHEN + MAPPER.acceptJsonFormatVisitor(BigDecimalWithAvroDecimalAnnotationWrapper.class, gen); + // actualSchema = MAPPER.schemaFor(BigDecimalWithAvroDecimalAnnotationWrapper.class) would be enough in this case + // because logical types are disabled by default. + final Schema actualSchema = gen.getGeneratedSchema().getAvroSchema(); + + System.out.println(BigDecimalWithAvroDecimalAnnotationWrapper.class.getSimpleName() + " schema:" + actualSchema.toString(true)); + + // THEN + assertThat(actualSchema.getField("bigDecimalValue")).isNotNull(); + Schema bigDecimalValue = actualSchema.getField("bigDecimalValue").schema(); + assertThat(bigDecimalValue.getType()).isEqualTo(Schema.Type.STRING); + assertThat(bigDecimalValue.getLogicalType()).isNull(); + assertThat(bigDecimalValue.getProp("java-class")).isEqualTo("java.math.BigDecimal"); + } + + @Test + public void testSchemaCreation_withLogicalTypesEnabled_onBigDecimalWithAvroDecimalAnnotation() throws JsonMappingException { + // GIVEN + AvroSchemaGenerator gen = new AvroSchemaGenerator() + .enableLogicalTypes(); + + // WHEN + MAPPER.acceptJsonFormatVisitor(BigDecimalWithAvroDecimalAnnotationWrapper.class, gen); + final Schema actualSchema = gen.getGeneratedSchema().getAvroSchema(); + + System.out.println(BigDecimalWithAvroDecimalAnnotationWrapper.class.getSimpleName() + " schema:" + actualSchema.toString(true)); + + // THEN + assertThat(actualSchema.getField("bigDecimalValue")).isNotNull(); + Schema bigDecimalValue = actualSchema.getField("bigDecimalValue").schema(); + assertThat(bigDecimalValue.getType()).isEqualTo(Schema.Type.BYTES); + assertThat(bigDecimalValue.getLogicalType()).isEqualTo(LogicalTypes.decimal(10, 2)); + assertThat(bigDecimalValue.getProp("java-class")).isNull(); + } + + static class BigDecimalWithAvroDecimalAnnotationToFixedWrapper { + @JsonProperty(required = true) // field is required to have simpler avro schema + @AvroFixedSize(typeName = "BigDecimalWithAvroDecimalAnnotationToFixedWrapper", size = 10) + @AvroDecimal(precision = 6, scale = 3) + public final BigDecimal bigDecimalValue; + + public BigDecimalWithAvroDecimalAnnotationToFixedWrapper(BigDecimal bigDecimalValue) { + this.bigDecimalValue = bigDecimalValue; + } + } + + @Test + public void testSchemaCreation_withLogicalTypesEnabled_onBigDecimalWithAvroDecimalAnnotationToFixed() throws JsonMappingException { + // GIVEN + AvroSchemaGenerator gen = new AvroSchemaGenerator() + .enableLogicalTypes(); + + // WHEN + MAPPER.acceptJsonFormatVisitor(BigDecimalWithAvroDecimalAnnotationToFixedWrapper.class, gen); + final Schema actualSchema = gen.getGeneratedSchema().getAvroSchema(); + + System.out.println(BigDecimalWithAvroDecimalAnnotationToFixedWrapper.class.getSimpleName() + " schema:" + actualSchema.toString(true)); + + // THEN + assertThat(actualSchema.getField("bigDecimalValue")).isNotNull(); + + Schema bigDecimalValue = actualSchema.getField("bigDecimalValue").schema(); + assertThat(bigDecimalValue.getType()).isEqualTo(Schema.Type.FIXED); + assertThat(bigDecimalValue.getFixedSize()).isEqualTo(10); + assertThat(bigDecimalValue.getLogicalType()).isEqualTo(LogicalTypes.decimal(6, 3)); + assertThat(bigDecimalValue.getProp("java-class")).isNull(); + } +} diff --git a/avro/src/test/java/com/fasterxml/jackson/dataformat/avro/BigDecimal_serialization_and_deserializationTest.java b/avro/src/test/java/com/fasterxml/jackson/dataformat/avro/BigDecimal_serialization_and_deserializationTest.java new file mode 100644 index 000000000..97e534314 --- /dev/null +++ b/avro/src/test/java/com/fasterxml/jackson/dataformat/avro/BigDecimal_serialization_and_deserializationTest.java @@ -0,0 +1,175 @@ +package com.fasterxml.jackson.dataformat.avro; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.math.BigDecimal; + +import static org.assertj.core.api.Assertions.assertThat; + +public class BigDecimal_serialization_and_deserializationTest extends AvroTestBase { + private static final AvroMapper MAPPER = new AvroMapper(); + + static class BigDecimalAndName { + public final BigDecimal bigDecimalValue; + public final String name; + + @JsonCreator + public BigDecimalAndName( + @JsonProperty("bigDecimalValue") BigDecimal bigDecimalValue, + @JsonProperty("name") String name) { + this.bigDecimalValue = bigDecimalValue; + this.name = name; + } + } + + // By default, BigDecimal is serialized to string + public void testSerialization_toString() throws Exception { + // GIVEN + String schemaString = "{" + + " \"type\" : \"record\"," + + " \"name\" : \"BigDecimalAndName\"," + + " \"namespace\" : \"test\"," + + " \"fields\" : [ {" + + " \"name\" : \"bigDecimalValue\"," + + " \"type\" : {" + + " \"type\" : \"string\"," + + " \"java-class\" : \"java.math.BigDecimal\"" + + " }" + + " }, {" + + " \"name\" : \"name\"," + + " \"type\" : \"string\"" + + " } ]" + + "}"; + + AvroSchema schema = MAPPER.schemaFrom(schemaString); + + // WHEN - serialize + byte[] bytes = MAPPER.writer(schema) + .writeValueAsBytes(new BigDecimalAndName(BigDecimal.valueOf(42.2), "peter")); + + // THEN + assertThat(bytes).isEqualTo(new byte[]{ + // bigDecimalValue + 0x08, // -> 4 dec - bigDecimalValue property string value length + 0x34, 0x32, 0x2E, 0x32, // -> "42.2" in ASCII + // name + 0x0A, // -> 5 dec - name property string length + 0x70, 0x65, 0x74, 0x65, 0x72 // -> "peter" in ASCII + }); + + // WHEN - deserialize + BigDecimalAndName result = MAPPER.reader(schema) + .forType(BigDecimalAndName.class) + .readValue(bytes); + + // THEN + assertThat(result.bigDecimalValue).isEqualTo(BigDecimal.valueOf(42.2)); + assertThat(result.name).isEqualTo("peter"); + } + + public void testSerialization_toBytesWithLogicalTypeDecimal() throws Exception { + // GIVEN + String schemaString = "{" + + " \"type\" : \"record\"," + + " \"name\" : \"BigDecimalAndName\"," + + " \"namespace\" : \"test\"," + + " \"fields\" : [ {" + + " \"name\" : \"bigDecimalValue\"," + + " \"type\" : [ \"null\", {" + + " \"type\" : \"bytes\"," + + " \"logicalType\" : \"decimal\"," + + " \"precision\" : 10," + + " \"scale\" : 2" + + " } ]" + + " }, {" + + " \"name\" : \"name\"," + + " \"type\" : [ \"null\", \"string\" ]" + + " } ]" + + "}"; + + AvroSchema schema = MAPPER.schemaFrom(schemaString); + + // WHEN - serialize + byte[] bytes = MAPPER.writer(schema) + .writeValueAsBytes(new BigDecimalAndName( + new BigDecimal("42.2"), + "peter")); + // THEN + assertThat(bytes).isEqualTo(new byte[]{ + // bigDecimalValue + 0x02, // -> 1 dec - second bigDecimalValue property type (bytes) + 0x04, // -> 2 dec - bigDecimalValue property bytes length + 0x10, 0x7C, // -> 0x107C -> 4220 dec - it is 42.2 value in scale 2. + // name + 0x02, // 1 dec - second name property type (string) + 0x0A, // -> 5 dec - name property string length + 0x70, 0x65, 0x74, 0x65, 0x72 // -> "peter" in ASCII + }); + + // WHEN - deserialize + BigDecimalAndName result = MAPPER.reader(schema) + .forType(BigDecimalAndName.class) + .readValue(bytes); + + // THEN + // Because scale of decimal logical type is 2, result is with 2 decimal places + assertThat(result.bigDecimalValue).isEqualTo(new BigDecimal("42.20")); + assertThat(result.name).isEqualTo("peter"); + } + + public void testSerialization_toFixedWithLogicalTypeDecimal() throws Exception { + // GIVEN + String schemaString = "{" + + " \"type\" : \"record\"," + + " \"name\" : \"BigDecimalAndName\"," + + " \"namespace\" : \"com.fasterxml.jackson.dataformat.avro.BigDecimalTest\"," + + " \"fields\" : [ {" + + " \"name\" : \"bigDecimalValue\"," + + " \"type\" : [ \"null\", {" + + " \"type\" : \"fixed\"," + + " \"name\" : \"BigDecimalValueType\"," + + " \"namespace\" : \"\"," + + " \"size\" : 10," + + " \"logicalType\" : \"decimal\"," + + " \"precision\" : 10," + + " \"scale\" : 2" + + " } ]" + + " }, {" + + " \"name\" : \"name\"," + + " \"type\" : [ \"null\", \"string\" ]" + + " } ]" + + "}"; + + AvroSchema schema = MAPPER.schemaFrom(schemaString); + + // WHEN - serialize + byte[] bytes = MAPPER.writer(schema) + .writeValueAsBytes(new BigDecimalAndName( + new BigDecimal("42.2"), + "peter")); + + // THEN + assertThat(bytes).isEqualTo(new byte[]{ + // bigDecimalValue + 0x02, // -> 1 dec - second bigDecimalValue property type (bytes) + // 10 bytes long fixed value + 0x00 ,0x00 ,0x00 ,0x00 ,0x00 ,0x00 ,0x00 ,0x00 ,0x10 ,0x7C, // -> 0x107C -> 4220 dec - it is 42.2 value in scale 2. + // name + 0x02, // 1 dec - second name property type (string) + 0x0A, // -> 5 dec - name property string length + 0x70, 0x65, 0x74, 0x65, 0x72 // -> "peter" in ASCII + }); + + // WHEN - deserialize + BigDecimalAndName result = MAPPER.reader(schema) + .forType(BigDecimalAndName.class) + .readValue(bytes); + + // THEN + // Because scale of decimal logical type is 2, result is with 2 decimal places + assertThat(result.bigDecimalValue).isEqualTo(new BigDecimal("42.20")); + assertThat(result.name).isEqualTo("peter"); + } + +} diff --git a/release-notes/CREDITS-2.x b/release-notes/CREDITS-2.x index 20b109095..82fd2be1c 100644 --- a/release-notes/CREDITS-2.x +++ b/release-notes/CREDITS-2.x @@ -219,12 +219,17 @@ Michal Foksa (MichalFoksa@github) (2.13.0) * Contributed #290: (avro) Generate logicalType switch (2.13.0) +* Contributed fix for #308: (avro) Incorrect serialization for `LogicalType.Decimal` + (Java `BigDecimal`) * Contributed #310: (avro) Avro schema generation: allow override namespace with new `@AvroNamespace` annotation (2.14.0) * Contributed #494: Avro Schema generation: allow mapping Java Enum properties to Avro String values (2.18.0) +* Contributed fix for #535: (avro) AvroSchemaGenerator: logicalType(s) never set + for non-date classes + (2.19.0) * Contributed #536: (avro) Add Logical Type support for `java.util.UUID` (2.19.0) @@ -357,3 +362,13 @@ Robert Noack (@mr-robert) Knut Wannheden (@knutwannheden) * Contributed #518: Should not read past end for CBOR string values (2.18.1) + +Idan Sheinberg (@sheinbergon) + * Reported #308: (avro) Incorrect serialization for `LogicalType.Decimal` (Java + `BigDecimal`) + (2.19.0) + +Cormac Redmond (@credmond) + * Reported #535: (avro) AvroSchemaGenerator: logicalType(s) never set for + non-Date classes + (2.19.0) diff --git a/release-notes/VERSION-2.x b/release-notes/VERSION-2.x index c60c55636..8f770bcc2 100644 --- a/release-notes/VERSION-2.x +++ b/release-notes/VERSION-2.x @@ -16,6 +16,12 @@ Active maintainers: 2.19.0 (not yet released) +#308: (avro) Incorrect serialization for `LogicalType.Decimal` (Java `BigDecimal`) + (reported by Idan S) + (fix contributed by Michal F) +#535: (avro) AvroSchemaGenerator: logicalType(s) never set for non-date classes + (reported by Cormac R) + (fix contributed by Michal F) #536: (avro) Add Logical Type support for `java.util.UUID` (contributed by Michal F)