Skip to content

Commit 52705c8

Browse files
committed
@Decimal annotation created. Avro schema for a field annotated with @Decimal is created with logical type decimal and type either bytes (by default) or fixed.
1 parent d12d0c3 commit 52705c8

File tree

3 files changed

+127
-9
lines changed

3 files changed

+127
-9
lines changed
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package com.fasterxml.jackson.dataformat.avro.annotation;
2+
3+
import java.lang.annotation.ElementType;
4+
import java.lang.annotation.Retention;
5+
import java.lang.annotation.RetentionPolicy;
6+
import java.lang.annotation.Target;
7+
8+
/**
9+
* Instructs the {@link com.fasterxml.jackson.dataformat.avro.schema.AvroSchemaGenerator AvroSchemaGenerator}
10+
* to declare the annotated property's logical type as "decimal" ({@link org.apache.avro.LogicalTypes.Decimal}).
11+
* By default, the Avro type is "bytes" ({@link org.apache.avro.Schema.Type#BYTES}), unless the field is also
12+
* annotated with {@link com.fasterxml.jackson.dataformat.avro.AvroFixedSize}, in which case the Avro type
13+
* will be "fixed" ({@link org.apache.avro.Schema.Type#FIXED}).
14+
* <p>
15+
* This annotation is only used during Avro schema generation and does not affect data serialization
16+
* or deserialization.
17+
*
18+
* @since 2.19
19+
*/
20+
@Target({ElementType.ANNOTATION_TYPE, ElementType.FIELD})
21+
@Retention(RetentionPolicy.RUNTIME)
22+
public @interface Decimal {
23+
24+
/**
25+
* Maximum precision of decimals stored in this type.
26+
*/
27+
int precision();
28+
29+
/**
30+
* Scale must be zero or a positive integer less than or equal to the precision.
31+
*/
32+
int scale() default 0;
33+
34+
}

avro/src/main/java/com/fasterxml/jackson/dataformat/avro/schema/RecordVisitor.java

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
import java.util.List;
55
import java.util.Map;
66

7+
import com.fasterxml.jackson.dataformat.avro.annotation.Decimal;
8+
import org.apache.avro.LogicalTypes;
79
import org.apache.avro.Schema;
810
import org.apache.avro.Schema.Type;
911
import org.apache.avro.reflect.AvroMeta;
@@ -141,17 +143,26 @@ public void optionalProperty(String name, JsonFormatVisitable handler,
141143

142144
protected Schema.Field schemaFieldForWriter(BeanProperty prop, boolean optional) throws JsonMappingException
143145
{
144-
Schema writerSchema;
146+
Schema writerSchema = null;
145147
// Check if schema for property is overridden
146148
AvroSchema schemaOverride = prop.getAnnotation(AvroSchema.class);
147149
if (schemaOverride != null) {
148150
Schema.Parser parser = new Schema.Parser();
149151
writerSchema = parser.parse(schemaOverride.value());
150152
} else {
151-
AvroFixedSize fixedSize = prop.getAnnotation(AvroFixedSize.class);
152-
if (fixedSize != null) {
153+
if (prop.getAnnotation(AvroFixedSize.class) != null) {
154+
AvroFixedSize fixedSize = prop.getAnnotation(AvroFixedSize.class);
153155
writerSchema = Schema.createFixed(fixedSize.typeName(), null, fixedSize.typeNamespace(), fixedSize.size());
154-
} else {
156+
}
157+
if (_visitorWrapper.isLogicalTypesEnabled() && prop.getAnnotation(Decimal.class) != null) {
158+
if (writerSchema == null) {
159+
writerSchema = Schema.create(Type.BYTES);
160+
}
161+
Decimal decimal = prop.getAnnotation(Decimal.class);
162+
writerSchema = LogicalTypes.decimal(decimal.precision(), decimal.scale())
163+
.addToSchema(writerSchema);
164+
}
165+
if (writerSchema == null) {
155166
JsonSerializer<?> ser = null;
156167

157168
// 23-Nov-2012, tatu: Ideally shouldn't need to do this but...
@@ -204,7 +215,7 @@ protected Schema.Field schemaFieldForWriter(BeanProperty prop, boolean optional)
204215

205216
/**
206217
* A union schema with a default value must always have the schema branch corresponding to the default value first, or Avro will print a
207-
* warning complaining that the default value is not compatible. If {@code schema} is a {@link Schema.Type#UNION UNION} schema and
218+
* warning complaining that the default value is not compatible. If {@code schema} is a {@link Type#UNION UNION} schema and
208219
* {@code defaultValue} is non-{@code null}, this finds the appropriate branch in the union and reorders the union so that it is first.
209220
*
210221
* @param schema

avro/src/test/java/com/fasterxml/jackson/dataformat/avro/BigDecimalTest.java

Lines changed: 77 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,85 @@
22

33
import com.fasterxml.jackson.annotation.JsonCreator;
44
import com.fasterxml.jackson.annotation.JsonProperty;
5+
import com.fasterxml.jackson.databind.JsonMappingException;
6+
import com.fasterxml.jackson.dataformat.avro.annotation.Decimal;
7+
import com.fasterxml.jackson.dataformat.avro.schema.AvroSchemaGenerator;
8+
import org.apache.avro.LogicalTypes;
9+
import org.apache.avro.Schema;
10+
import org.junit.Test;
511

612
import java.math.BigDecimal;
713

14+
import static org.assertj.core.api.Assertions.assertThat;
15+
816
public class BigDecimalTest extends AvroTestBase
917
{
18+
private static final AvroMapper MAPPER = new AvroMapper();
19+
20+
static class BigDecimalWithDecimalAnnotationToBytesWrapper {
21+
@JsonProperty(required = true) // field is made required only to have simpler avro schema
22+
@Decimal(precision = 10, scale = 2)
23+
public BigDecimal bigDecimalValue;
24+
25+
public BigDecimalWithDecimalAnnotationToBytesWrapper(BigDecimal bigDecimalValue) {
26+
this.bigDecimalValue = bigDecimalValue;
27+
}
28+
}
29+
30+
@Test
31+
public void testSchemaCreationOnBigDecimalWithDecimalAnnotationToBytes() throws JsonMappingException {
32+
// GIVEN
33+
AvroSchemaGenerator gen = new AvroSchemaGenerator()
34+
.enableLogicalTypes();
35+
36+
// WHEN
37+
MAPPER.acceptJsonFormatVisitor(BigDecimalWithDecimalAnnotationToBytesWrapper.class, gen);
38+
final Schema actualSchema = gen.getGeneratedSchema().getAvroSchema();
39+
40+
System.out.println(BigDecimalWithDecimalAnnotationToBytesWrapper.class.getSimpleName() + " schema:\n" + actualSchema.toString(true));
41+
42+
// THEN
43+
assertThat(actualSchema.getField("bigDecimalValue")).isNotNull();
44+
45+
Schema bigDecimalValue = actualSchema.getField("bigDecimalValue").schema();
46+
assertThat(bigDecimalValue.getType()).isEqualTo(Schema.Type.BYTES);
47+
assertThat(bigDecimalValue.getLogicalType()).isEqualTo(LogicalTypes.decimal(10, 2));
48+
assertThat(bigDecimalValue.getProp("java-class")).isNull();
49+
}
50+
51+
static class BigDecimalWithDecimalAnnotationToFixedWrapper {
52+
@JsonProperty(required = true) // field is made required only to have simpler avro schema
53+
@AvroFixedSize(typeName = "BigDecimalWithDecimalAnnotationToFixedWrapper", size = 10)
54+
@Decimal(precision = 6, scale = 3)
55+
public BigDecimal bigDecimalValue;
56+
57+
public BigDecimalWithDecimalAnnotationToFixedWrapper(BigDecimal bigDecimalValue) {
58+
this.bigDecimalValue = bigDecimalValue;
59+
}
60+
}
61+
62+
@Test
63+
public void testSchemaCreationOnBigDecimalWithDecimalAnnotationToFixed() throws JsonMappingException {
64+
// GIVEN
65+
AvroSchemaGenerator gen = new AvroSchemaGenerator()
66+
.enableLogicalTypes();
67+
68+
// WHEN
69+
MAPPER.acceptJsonFormatVisitor(BigDecimalWithDecimalAnnotationToFixedWrapper.class, gen);
70+
final Schema actualSchema = gen.getGeneratedSchema().getAvroSchema();
71+
72+
System.out.println(BigDecimalWithDecimalAnnotationToFixedWrapper.class.getSimpleName() + " schema:\n" + actualSchema.toString(true));
73+
74+
// THEN
75+
assertThat(actualSchema.getField("bigDecimalValue")).isNotNull();
76+
77+
Schema bigDecimalValue = actualSchema.getField("bigDecimalValue").schema();
78+
assertThat(bigDecimalValue.getType()).isEqualTo(Schema.Type.FIXED);
79+
assertThat(bigDecimalValue.getFixedSize()).isEqualTo(10);
80+
assertThat(bigDecimalValue.getLogicalType()).isEqualTo(LogicalTypes.decimal(6, 3));
81+
assertThat(bigDecimalValue.getProp("java-class")).isNull();
82+
}
83+
1084
public static class NamedAmount {
1185
public final String name;
1286
public final BigDecimal amount;
@@ -20,13 +94,12 @@ public NamedAmount(@JsonProperty("name") String name,
2094
}
2195

2296
public void testSerializeBigDecimal() throws Exception {
23-
AvroMapper mapper = newMapper();
24-
AvroSchema schema = mapper.schemaFor(NamedAmount.class);
97+
AvroSchema schema = MAPPER.schemaFor(NamedAmount.class);
2598

26-
byte[] bytes = mapper.writer(schema)
99+
byte[] bytes = MAPPER.writer(schema)
27100
.writeValueAsBytes(new NamedAmount("peter", 42.0));
28101

29-
NamedAmount result = mapper.reader(schema).forType(NamedAmount.class).readValue(bytes);
102+
NamedAmount result = MAPPER.reader(schema).forType(NamedAmount.class).readValue(bytes);
30103

31104
assertEquals("peter", result.name);
32105
assertEquals(BigDecimal.valueOf(42.0), result.amount);

0 commit comments

Comments
 (0)