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