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