Skip to content

Commit cf3bdb3

Browse files
committed
Merge branch '2.x' into 3.x
2 parents 51dc6fa + 986cc06 commit cf3bdb3

File tree

5 files changed

+395
-24
lines changed

5 files changed

+395
-24
lines changed

.github/workflows/main.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ jobs:
5555
run: ./mvnw -B -q -ff -ntp test
5656
- name: Publish code coverage
5757
if: ${{ matrix.release_build && github.event_name != 'pull_request' }}
58-
uses: codecov/codecov-action@ad3126e916f78f00edff4ed0317cf185271ccc2d # v5.4.2
58+
uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 # v5.4.3
5959
with:
6060
token: ${{ secrets.CODECOV_TOKEN }}
6161
files: ./target/site/jacoco/jacoco.xml

avro/src/main/java/tools/jackson/dataformat/avro/schema/RecordVisitor.java

Lines changed: 86 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -29,58 +29,122 @@ public class RecordVisitor
2929
protected final VisitorFormatWrapperImpl _visitorWrapper;
3030

3131
/**
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.
3434
*/
3535
protected final boolean _overridden;
3636

3737
protected final boolean _cfgAddNullDefaults;
3838

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
3971
protected Schema _avroSchema;
4072

73+
// !!! 19-May-2025: TODO: make final in 2.20
4174
protected List<Schema.Field> _fields = new ArrayList<>();
4275

43-
public RecordVisitor(SerializationContext p, JavaType type, VisitorFormatWrapperImpl visitorWrapper)
76+
public RecordVisitor(SerializationContext ctxt, JavaType type,
77+
VisitorFormatWrapperImpl visitorWrapper)
4478
{
45-
super(p);
79+
super(ctxt);
4680
_type = type;
4781
_visitorWrapper = visitorWrapper;
4882

49-
AvroFactory avroFactory = (AvroFactory) p.tokenStreamFactory();
83+
AvroFactory avroFactory = (AvroFactory) ctxt.tokenStreamFactory();
5084
_cfgAddNullDefaults = avroFactory.isEnabled(AvroWriteFeature.ADD_NULL_AS_DEFAULT_VALUE_IN_SCHEMA);
5185

5286
// Check if the schema for this record is overridden
53-
SerializationConfig config = p.getConfig();
87+
SerializationConfig config = ctxt.getConfig();
5488

5589
// 12-Oct-2019, tatu: VERY important: only get direct annotations, not for supertypes --
5690
// otherwise there's infinite loop awaiting for... some reason. Other parts of code
5791
// should probably check for loops but bit hard for me to fix as I did not author
5892
// 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);
6296
AvroSchema ann = annotations.getAnnotation(AvroSchema.class);
6397
if (ann != null) {
6498
_avroSchema = AvroSchemaHelper.parseJsonSchema(ann.value());
6599
_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;
77101
} 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;
79105
_overridden = false;
80106
AvroMeta meta = annotations.getAnnotation(AvroMeta.class);
81107
if (meta != null) {
82108
_avroSchema.addProp(meta.key(), meta.value());
83109
}
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+
}
84148
}
85149
_visitorWrapper.getSchemas().addSchema(type, _avroSchema);
86150
}
@@ -89,7 +153,7 @@ public RecordVisitor(SerializationContext p, JavaType type, VisitorFormatWrapper
89153
public Schema builtAvroSchema() {
90154
if (!_overridden) {
91155
// Assumption now is that we are done, so let's assign fields
92-
_avroSchema.setFields(_fields);
156+
_typeSchema.setFields(_fields);
93157
}
94158
return _avroSchema;
95159
}

0 commit comments

Comments
 (0)