@@ -26,13 +26,46 @@ public class RecordVisitor
26
26
protected final VisitorFormatWrapperImpl _visitorWrapper ;
27
27
28
28
/**
29
- * Tracks if the schema for this record has been overridden (by an annotation or other means), and calls to the {@code property} and
30
- * {@code optionalProperty} methods should be ignored.
29
+ * Tracks if the schema for this record has been overridden (by an annotation or other means),
30
+ * and calls to the {@code property} and {@code optionalProperty} methods should be ignored.
31
31
*/
32
32
protected final boolean _overridden ;
33
33
34
+ /**
35
+ * When Avro schema for this JavaType ({@code _type}) results in UNION of multiple Avro types,
36
+ * _typeSchema keeps track of which Avro type in the UNION represents this JavaType ({@code _type})
37
+ * so that fields of this JavaType can be set to the right Avro type by {@code builtAvroSchema()}.
38
+ *<br>
39
+ * Example:
40
+ * <pre>
41
+ * @JsonSubTypes({
42
+ * @JsonSubTypes.Type(value = Apple.class),
43
+ * @JsonSubTypes.Type(value = Pear.class) })
44
+ * class Fruit {}
45
+ *
46
+ * class Apple extends Fruit {}
47
+ * class Orange extends Fruit {}
48
+ * </pre>
49
+ * When {@code _type = Fruit.class}
50
+ * Then
51
+ * _avroSchema if Fruit.class is union of Fruit record, Apple record and Orange record schemas: [
52
+ * { name: Fruit, type: record, fields: [..] }, <--- _typeSchema points here
53
+ * { name: Apple, type: record, fields: [..] },
54
+ * { name: Orange, type: record, fields: [..]}
55
+ * ]
56
+ * _typeSchema points to Fruit.class without subtypes record schema
57
+ *
58
+ * FIXME: When _typeSchema is not null, then _overridden must be true, therefore (_overridden == true) can be replaced with (_typeSchema != null),
59
+ * but it might be considered API change cause _overridden has protected access modifier.
60
+ *
61
+ * @since 2.19.1
62
+ */
63
+ private final Schema _typeSchema ;
64
+
65
+ // !!! 19-May-2025: TODO: make final in 2.20
34
66
protected Schema _avroSchema ;
35
67
68
+ // !!! 19-May-2025: TODO: make final in 2.20
36
69
protected List <Schema .Field > _fields = new ArrayList <>();
37
70
38
71
public RecordVisitor (SerializerProvider p , JavaType type , VisitorFormatWrapperImpl visitorWrapper )
@@ -42,32 +75,62 @@ public RecordVisitor(SerializerProvider p, JavaType type, VisitorFormatWrapperIm
42
75
_visitorWrapper = visitorWrapper ;
43
76
// Check if the schema for this record is overridden
44
77
BeanDescription bean = getProvider ().getConfig ().introspectDirectClassAnnotations (_type );
45
- List <NamedType > subTypes = getProvider ().getAnnotationIntrospector ().findSubtypes (bean .getClassInfo ());
46
78
AvroSchema ann = bean .getClassInfo ().getAnnotation (AvroSchema .class );
47
79
if (ann != null ) {
48
80
_avroSchema = AvroSchemaHelper .parseJsonSchema (ann .value ());
49
81
_overridden = true ;
50
- } else if (subTypes != null && !subTypes .isEmpty ()) {
51
- List <Schema > unionSchemas = new ArrayList <>();
52
- try {
53
- for (NamedType subType : subTypes ) {
54
- JsonSerializer <?> ser = getProvider ().findValueSerializer (subType .getType ());
55
- VisitorFormatWrapperImpl visitor = _visitorWrapper .createChildWrapper ();
56
- ser .acceptJsonFormatVisitor (visitor , getProvider ().getTypeFactory ().constructType (subType .getType ()));
57
- unionSchemas .add (visitor .getAvroSchema ());
58
- }
59
- _avroSchema = Schema .createUnion (unionSchemas );
60
- _overridden = true ;
61
- } catch (JsonMappingException jme ) {
62
- throw new RuntimeException ("Failed to build schema" , jme );
63
- }
82
+ _typeSchema = null ;
64
83
} else {
84
+ // If Avro schema for this _type results in UNION I want to know Avro type where to assign fields
65
85
_avroSchema = AvroSchemaHelper .initializeRecordSchema (bean );
86
+ _typeSchema = _avroSchema ;
66
87
_overridden = false ;
67
88
AvroMeta meta = bean .getClassInfo ().getAnnotation (AvroMeta .class );
68
89
if (meta != null ) {
69
90
_avroSchema .addProp (meta .key (), meta .value ());
70
91
}
92
+
93
+ List <NamedType > subTypes = getProvider ().getAnnotationIntrospector ().findSubtypes (bean .getClassInfo ());
94
+ if (subTypes != null && !subTypes .isEmpty ()) {
95
+ // alreadySeenClasses prevents subType processing in endless loop
96
+ Set <Class <?>> alreadySeenClasses = new HashSet <>();
97
+ alreadySeenClasses .add (_type .getRawClass ());
98
+
99
+ // At this point calculating hashCode for _typeSchema fails with
100
+ // NPE because RecordSchema.fields is NULL
101
+ // (see org.apache.avro.Schema.RecordSchema#computeHash).
102
+ // Therefore, unionSchemas must not be HashSet (or any other type
103
+ // using hashCode() for equality check).
104
+ // Set ensures that each subType schema is once in resulting union.
105
+ // IdentityHashMap is used because it is using reference-equality.
106
+ final Set <Schema > unionSchemas = Collections .newSetFromMap (new IdentityHashMap <>());
107
+ // Initialize with this schema
108
+ if (_type .isConcrete ()) {
109
+ unionSchemas .add (_typeSchema );
110
+ }
111
+
112
+ try {
113
+ for (NamedType subType : subTypes ) {
114
+ if (!alreadySeenClasses .add (subType .getType ())) {
115
+ continue ;
116
+ }
117
+ JsonSerializer <?> ser = getProvider ().findValueSerializer (subType .getType ());
118
+ VisitorFormatWrapperImpl visitor = _visitorWrapper .createChildWrapper ();
119
+ ser .acceptJsonFormatVisitor (visitor , getProvider ().getTypeFactory ().constructType (subType .getType ()));
120
+ // Add subType schema into this union, unless it is already there.
121
+ Schema subTypeSchema = visitor .getAvroSchema ();
122
+ // When subType schema is union itself, include each its type into this union if not there already
123
+ if (subTypeSchema .getType () == Type .UNION ) {
124
+ unionSchemas .addAll (subTypeSchema .getTypes ());
125
+ } else {
126
+ unionSchemas .add (subTypeSchema );
127
+ }
128
+ }
129
+ _avroSchema = Schema .createUnion (new ArrayList <>(unionSchemas ));
130
+ } catch (JsonMappingException jme ) {
131
+ throw new RuntimeJsonMappingException ("Failed to build schema" , jme );
132
+ }
133
+ }
71
134
}
72
135
_visitorWrapper .getSchemas ().addSchema (type , _avroSchema );
73
136
}
@@ -76,7 +139,7 @@ public RecordVisitor(SerializerProvider p, JavaType type, VisitorFormatWrapperIm
76
139
public Schema builtAvroSchema () {
77
140
if (!_overridden ) {
78
141
// Assumption now is that we are done, so let's assign fields
79
- _avroSchema .setFields (_fields );
142
+ _typeSchema .setFields (_fields );
80
143
}
81
144
return _avroSchema ;
82
145
}
0 commit comments