@@ -26,13 +26,46 @@ public class RecordVisitor
2626 protected final VisitorFormatWrapperImpl _visitorWrapper ;
2727
2828 /**
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.
3131 */
3232 protected final boolean _overridden ;
3333
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
3466 protected Schema _avroSchema ;
3567
68+ // !!! 19-May-2025: TODO: make final in 2.20
3669 protected List <Schema .Field > _fields = new ArrayList <>();
3770
3871 public RecordVisitor (SerializerProvider p , JavaType type , VisitorFormatWrapperImpl visitorWrapper )
@@ -42,32 +75,62 @@ public RecordVisitor(SerializerProvider p, JavaType type, VisitorFormatWrapperIm
4275 _visitorWrapper = visitorWrapper ;
4376 // Check if the schema for this record is overridden
4477 BeanDescription bean = getProvider ().getConfig ().introspectDirectClassAnnotations (_type );
45- List <NamedType > subTypes = getProvider ().getAnnotationIntrospector ().findSubtypes (bean .getClassInfo ());
4678 AvroSchema ann = bean .getClassInfo ().getAnnotation (AvroSchema .class );
4779 if (ann != null ) {
4880 _avroSchema = AvroSchemaHelper .parseJsonSchema (ann .value ());
4981 _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 ;
6483 } else {
84+ // If Avro schema for this _type results in UNION I want to know Avro type where to assign fields
6585 _avroSchema = AvroSchemaHelper .initializeRecordSchema (bean );
86+ _typeSchema = _avroSchema ;
6687 _overridden = false ;
6788 AvroMeta meta = bean .getClassInfo ().getAnnotation (AvroMeta .class );
6889 if (meta != null ) {
6990 _avroSchema .addProp (meta .key (), meta .value ());
7091 }
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+ }
71134 }
72135 _visitorWrapper .getSchemas ().addSchema (type , _avroSchema );
73136 }
@@ -76,7 +139,7 @@ public RecordVisitor(SerializerProvider p, JavaType type, VisitorFormatWrapperIm
76139 public Schema builtAvroSchema () {
77140 if (!_overridden ) {
78141 // Assumption now is that we are done, so let's assign fields
79- _avroSchema .setFields (_fields );
142+ _typeSchema .setFields (_fields );
80143 }
81144 return _avroSchema ;
82145 }
0 commit comments