Skip to content

Commit 4ed2bf0

Browse files
committed
[Avro] Add support for @AvroSchema, @JsonPropertyDescription, and @JsonClassDescription
1 parent 45040df commit 4ed2bf0

File tree

2 files changed

+178
-38
lines changed

2 files changed

+178
-38
lines changed
Lines changed: 79 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,21 @@
11
package com.fasterxml.jackson.dataformat.avro.schema;
22

3-
import com.fasterxml.jackson.databind.*;
3+
import java.util.ArrayList;
4+
import java.util.List;
5+
6+
import com.fasterxml.jackson.databind.BeanProperty;
7+
import com.fasterxml.jackson.databind.JavaType;
8+
import com.fasterxml.jackson.databind.JsonMappingException;
9+
import com.fasterxml.jackson.databind.JsonSerializer;
10+
import com.fasterxml.jackson.databind.SerializerProvider;
11+
import com.fasterxml.jackson.databind.introspect.AnnotatedClass;
412
import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonFormatVisitable;
513
import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonObjectFormatVisitor;
614
import com.fasterxml.jackson.databind.ser.BeanPropertyWriter;
715
import com.fasterxml.jackson.dataformat.avro.AvroFixedSize;
8-
import org.apache.avro.Schema;
916

10-
import java.util.ArrayList;
11-
import java.util.List;
17+
import org.apache.avro.Schema;
18+
import org.apache.avro.reflect.AvroSchema;
1219

1320
public class RecordVisitor
1421
extends JsonObjectFormatVisitor.Base
@@ -19,6 +26,8 @@ public class RecordVisitor
1926
protected final DefinedSchemas _schemas;
2027

2128
protected Schema _avroSchema;
29+
30+
protected boolean _overridden;
2231

2332
protected List<Schema.Field> _fields = new ArrayList<Schema.Field>();
2433

@@ -27,16 +36,27 @@ public RecordVisitor(SerializerProvider p, JavaType type, DefinedSchemas schemas
2736
super(p);
2837
_type = type;
2938
_schemas = schemas;
30-
_avroSchema = Schema.createRecord(AvroSchemaHelper.getName(type),
31-
"Schema for "+type.toCanonical(),
32-
AvroSchemaHelper.getNamespace(type), false);
39+
// Check if the schema for this record is overridden
40+
AnnotatedClass ac = getProvider().getConfig().introspectDirectClassAnnotations(_type).getClassInfo();
41+
AvroSchema ann = ac.getAnnotation(AvroSchema.class);
42+
if (ann != null) {
43+
Schema.Parser parser = new Schema.Parser();
44+
_avroSchema = parser.parse(ann.value());
45+
_overridden = true;
46+
} else {
47+
String description = getProvider().getAnnotationIntrospector().findClassDescription(ac);
48+
_avroSchema = Schema.createRecord(AvroSchemaHelper.getName(type), description, AvroSchemaHelper.getNamespace(type), false);
49+
_overridden = false;
50+
}
3351
schemas.addSchema(type, _avroSchema);
3452
}
3553

3654
@Override
3755
public Schema builtAvroSchema() {
38-
// Assumption now is that we are done, so let's assign fields
39-
_avroSchema.setFields(_fields);
56+
if (!_overridden) {
57+
// Assumption now is that we are done, so let's assign fields
58+
_avroSchema.setFields(_fields);
59+
}
4060
return _avroSchema;
4161
}
4262

@@ -49,14 +69,19 @@ public Schema builtAvroSchema() {
4969
@Override
5070
public void property(BeanProperty writer) throws JsonMappingException
5171
{
52-
Schema schema = schemaForWriter(writer);
53-
_fields.add(new Schema.Field(writer.getName(), schema, null, null));
72+
if (_overridden) {
73+
return;
74+
}
75+
_fields.add(schemaFieldForWriter(writer, false));
5476
}
5577

5678
@Override
5779
public void property(String name, JsonFormatVisitable handler,
5880
JavaType type) throws JsonMappingException
5981
{
82+
if (_overridden) {
83+
return;
84+
}
6085
VisitorFormatWrapperImpl wrapper = new VisitorFormatWrapperImpl(_schemas, getProvider());
6186
handler.acceptJsonFormatVisitor(wrapper, type);
6287
Schema schema = wrapper.getAvroSchema();
@@ -65,21 +90,19 @@ public void property(String name, JsonFormatVisitable handler,
6590

6691
@Override
6792
public void optionalProperty(BeanProperty writer) throws JsonMappingException {
68-
Schema schema = schemaForWriter(writer);
69-
/* 23-Nov-2012, tatu: Actually let's also assume that primitive type values
70-
* are required, as Jackson does not distinguish whether optional has been
71-
* defined, or is merely the default setting.
72-
*/
73-
if (!writer.getType().isPrimitive()) {
74-
schema = AvroSchemaHelper.unionWithNull(schema);
93+
if (_overridden) {
94+
return;
7595
}
76-
_fields.add(new Schema.Field(writer.getName(), schema, null, null));
96+
_fields.add(schemaFieldForWriter(writer, true));
7797
}
7898

7999
@Override
80100
public void optionalProperty(String name, JsonFormatVisitable handler,
81101
JavaType type) throws JsonMappingException
82102
{
103+
if (_overridden) {
104+
return;
105+
}
83106
VisitorFormatWrapperImpl wrapper = new VisitorFormatWrapperImpl(_schemas, getProvider());
84107
handler.acceptJsonFormatVisitor(wrapper, type);
85108
Schema schema = wrapper.getAvroSchema();
@@ -95,29 +118,47 @@ public void optionalProperty(String name, JsonFormatVisitable handler,
95118
/**********************************************************************
96119
*/
97120

98-
protected Schema schemaForWriter(BeanProperty prop) throws JsonMappingException
121+
protected Schema.Field schemaFieldForWriter(BeanProperty prop, boolean optional) throws JsonMappingException
99122
{
100-
AvroFixedSize fixedSize = prop.getAnnotation(AvroFixedSize.class);
101-
if (fixedSize != null) {
102-
return Schema.createFixed(fixedSize.typeName(), null, fixedSize.typeNamespace(), fixedSize.size());
103-
}
123+
Schema writerSchema;
124+
// Check if schema for property is overridden
125+
AvroSchema schemaOverride = prop.getAnnotation(AvroSchema.class);
126+
if (schemaOverride != null) {
127+
Schema.Parser parser = new Schema.Parser();
128+
writerSchema = parser.parse(schemaOverride.value());
129+
} else {
130+
AvroFixedSize fixedSize = prop.getAnnotation(AvroFixedSize.class);
131+
if (fixedSize != null) {
132+
writerSchema = Schema.createFixed(fixedSize.typeName(), null, fixedSize.typeNamespace(), fixedSize.size());
133+
} else {
134+
JsonSerializer<?> ser = null;
104135

105-
JsonSerializer<?> ser = null;
136+
// 23-Nov-2012, tatu: Ideally shouldn't need to do this but...
137+
if (prop instanceof BeanPropertyWriter) {
138+
BeanPropertyWriter bpw = (BeanPropertyWriter) prop;
139+
ser = bpw.getSerializer();
140+
}
141+
final SerializerProvider prov = getProvider();
142+
if (ser == null) {
143+
if (prov == null) {
144+
throw JsonMappingException.from(prov, "SerializerProvider missing for RecordVisitor");
145+
}
146+
ser = prov.findValueSerializer(prop.getType(), prop);
147+
}
148+
VisitorFormatWrapperImpl visitor = new VisitorFormatWrapperImpl(_schemas, prov);
149+
ser.acceptJsonFormatVisitor(visitor, prop.getType());
150+
writerSchema = visitor.getAvroSchema();
151+
}
106152

107-
// 23-Nov-2012, tatu: Ideally shouldn't need to do this but...
108-
if (prop instanceof BeanPropertyWriter) {
109-
BeanPropertyWriter bpw = (BeanPropertyWriter) prop;
110-
ser = bpw.getSerializer();
111-
}
112-
final SerializerProvider prov = getProvider();
113-
if (ser == null) {
114-
if (prov == null) {
115-
throw JsonMappingException.from(prov, "SerializerProvider missing for RecordVisitor");
153+
/* 23-Nov-2012, tatu: Actually let's also assume that primitive type values
154+
* are required, as Jackson does not distinguish whether optional has been
155+
* defined, or is merely the default setting.
156+
*/
157+
if (optional && !prop.getType().isPrimitive()) {
158+
writerSchema = AvroSchemaHelper.unionWithNull(writerSchema);
116159
}
117-
ser = prov.findValueSerializer(prop.getType(), prop);
118160
}
119-
VisitorFormatWrapperImpl visitor = new VisitorFormatWrapperImpl(_schemas, prov);
120-
ser.acceptJsonFormatVisitor(visitor, prop.getType());
121-
return visitor.getAvroSchema();
161+
String description = getProvider().getAnnotationIntrospector().findPropertyDescription(prop.getMember());
162+
return new Schema.Field(prop.getName(), writerSchema, description, null);
122163
}
123164
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
package com.fasterxml.jackson.dataformat.avro.interop.annotations;
2+
3+
import com.fasterxml.jackson.annotation.JsonClassDescription;
4+
import com.fasterxml.jackson.annotation.JsonPropertyDescription;
5+
import com.fasterxml.jackson.dataformat.avro.interop.ApacheAvroInteropUtil;
6+
import com.fasterxml.jackson.dataformat.avro.interop.InteropTestBase;
7+
8+
import lombok.Data;
9+
import org.apache.avro.Schema;
10+
import org.apache.avro.reflect.AvroSchema;
11+
import org.apache.avro.reflect.Nullable;
12+
import org.junit.Test;
13+
14+
import static org.assertj.core.api.Assertions.assertThat;
15+
16+
/**
17+
* Tests that {@code @AvroSchema} is handled correctly. Specifically:
18+
* <ul>
19+
* <li>When present on a type, it completely and wholly overrides any other schema-related information on that type and its fields,
20+
* including name, meta properties, defaults, and docs</li>
21+
* <li>When present on a field, it completely and wholly overrides any other schema-related information on that field (but not
22+
* other properties, like name, defaults, docs, or meta properties)</li>
23+
* </ul>
24+
* <p>
25+
* Additionally, tests that class and property descriptions are picked up correctly when using Jackson annotations
26+
*/
27+
public class AvroSchemaTest extends InteropTestBase {
28+
29+
@Data
30+
@AvroSchema("{\"type\":\"string\"}")
31+
public static class OverriddenClassSchema {
32+
33+
private int field;
34+
}
35+
36+
@Data
37+
@JsonClassDescription("A cool class!")
38+
public static class OverriddenFieldSchema {
39+
40+
@AvroSchema("{\"type\":\"int\"}")
41+
private String myField;
42+
43+
@Nullable
44+
@JsonPropertyDescription("the best field in the world")
45+
private OverriddenClassSchema recursiveOverride;
46+
47+
@Nullable
48+
@AvroSchema("{\"type\":\"long\"}")
49+
private OverriddenClassSchema precedenceField;
50+
}
51+
52+
@Test
53+
public void testJacksonClassDescription() {
54+
Schema schema = ApacheAvroInteropUtil.getJacksonSchema(OverriddenFieldSchema.class);
55+
//
56+
assertThat(schema.getDoc()).isEqualTo("A cool class!");
57+
}
58+
59+
@Test
60+
public void testJacksonPropertyDescription() {
61+
Schema schema = ApacheAvroInteropUtil.getJacksonSchema(OverriddenFieldSchema.class);
62+
//
63+
assertThat(schema.getField("recursiveOverride").doc()).isEqualTo("the best field in the world");
64+
}
65+
66+
@Test
67+
public void testTypeOverride() {
68+
Schema schema = schemaFunctor.apply(OverriddenClassSchema.class);
69+
//
70+
assertThat(schema.getType()).isEqualTo(Schema.Type.STRING);
71+
}
72+
73+
@Test
74+
public void testFieldOverride() {
75+
Schema schema = schemaFunctor.apply(OverriddenFieldSchema.class);
76+
//
77+
assertThat(schema.getType()).isEqualTo(Schema.Type.RECORD);
78+
assertThat(schema.getField("myField").schema().getType()).isEqualTo(Schema.Type.INT);
79+
}
80+
81+
@Test
82+
public void testRecursiveFieldOverride() {
83+
Schema schema = schemaFunctor.apply(OverriddenFieldSchema.class);
84+
//
85+
assertThat(schema.getType()).isEqualTo(Schema.Type.RECORD);
86+
assertThat(schema.getField("recursiveOverride").schema().getType()).isEqualTo(Schema.Type.UNION);
87+
assertThat(schema.getField("recursiveOverride").schema().getTypes().get(0).getType()).isEqualTo(Schema.Type.NULL);
88+
assertThat(schema.getField("recursiveOverride").schema().getTypes().get(1).getType()).isEqualTo(Schema.Type.STRING);
89+
}
90+
91+
@Test
92+
public void testOverridePrecedence() {
93+
Schema schema = schemaFunctor.apply(OverriddenFieldSchema.class);
94+
//
95+
assertThat(schema.getType()).isEqualTo(Schema.Type.RECORD);
96+
assertThat(schema.getField("precedenceField").schema().getType()).isEqualTo(Schema.Type.LONG);
97+
}
98+
99+
}

0 commit comments

Comments
 (0)