Skip to content

Commit 792e0d2

Browse files
committed
Prevention of StackOverflowError exception when base class is explicitly in @JsonSubTypes or @union annotations.
1 parent 88fa1dc commit 792e0d2

File tree

2 files changed

+107
-0
lines changed

2 files changed

+107
-0
lines changed

avro/src/main/java/com/fasterxml/jackson/dataformat/avro/schema/RecordVisitor.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,17 +85,26 @@ public RecordVisitor(SerializerProvider p, JavaType type, VisitorFormatWrapperIm
8585

8686
List<NamedType> subTypes = getProvider().getAnnotationIntrospector().findSubtypes(bean.getClassInfo());
8787
if (subTypes != null && !subTypes.isEmpty()) {
88+
// alreadySeenClasses prevents subType processing in endless loop
89+
Set<Class<?>> alreadySeenClasses = new HashSet<>();
90+
alreadySeenClasses.add(_type.getRawClass());
91+
8892
// At this point calculating hashCode for _typeSchema fails with NPE because RecordSchema.fields is NULL
8993
// see org.apache.avro.Schema.RecordSchema#computeHash.
9094
// Therefore, unionSchemas must not be HashSet (or any other type using hashCode() for equality check).
95+
// Set ensures that each subType schema is once in resulting union.
9196
// IdentityHashMap is used because it is using reference-equality.
9297
Set<Schema> unionSchemas = Collections.newSetFromMap(new IdentityHashMap<>());
9398
// Initialize with this schema
9499
if (_type.isConcrete()) {
95100
unionSchemas.add(_typeSchema);
96101
}
102+
97103
try {
98104
for (NamedType subType : subTypes) {
105+
if (!alreadySeenClasses.add(subType.getType())) {
106+
continue;
107+
}
99108
JsonSerializer<?> ser = getProvider().findValueSerializer(subType.getType());
100109
VisitorFormatWrapperImpl visitor = _visitorWrapper.createChildWrapper();
101110
ser.acceptJsonFormatVisitor(visitor, getProvider().getTypeFactory().constructType(subType.getType()));

avro/src/test/java/com/fasterxml/jackson/dataformat/avro/schema/PolymorphicTypeAnnotationsTest.java

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import com.fasterxml.jackson.dataformat.avro.AvroMapper;
66
import com.fasterxml.jackson.dataformat.avro.annotation.AvroNamespace;
77
import org.apache.avro.Schema;
8+
import org.apache.avro.reflect.Union;
89
import org.junit.jupiter.api.Test;
910

1011
import java.io.IOException;
@@ -195,4 +196,101 @@ public void class_is_referenced_twice_in_hierarchy_test() throws JsonMappingExce
195196
assertThat(actualSchema.getTypes()).containsExactlyInAnyOrder(heliumSchema, oxygenSchema);
196197
}
197198

199+
@JsonSubTypes({
200+
// Base class being explicitly in @JsonSubTypes led to StackOverflowError exception.
201+
@JsonSubTypes.Type(value = Image.class),
202+
@JsonSubTypes.Type(value = Jpeg.class),
203+
@JsonSubTypes.Type(value = Png.class),
204+
})
205+
@AvroNamespace(TEST_NAMESPACE) // @AvroNamespace makes it easier to create schema string representation
206+
private static class Image {
207+
}
208+
209+
private static final String IMAGE_ITSELF_SCHEMA_STR = "{\"type\":\"record\",\"name\":\"Image\",\"namespace\":\"test\",\"fields\":[]}";
210+
211+
private static class Jpeg extends Image {
212+
}
213+
214+
private static class Png extends Image {
215+
}
216+
217+
@Test
218+
public void base_class_explicitly_in_JsonSubTypes_annotation_test() throws IOException {
219+
// GIVEN
220+
final Schema imageItselfSchema = MAPPER.schemaFrom(IMAGE_ITSELF_SCHEMA_STR).getAvroSchema();
221+
final Schema jpegSchema = MAPPER.schemaFor(Jpeg.class).getAvroSchema();
222+
final Schema pngSchema = MAPPER.schemaFor(Png.class).getAvroSchema();
223+
224+
// WHEN
225+
Schema actualSchema = MAPPER.schemaFor(Image.class).getAvroSchema();
226+
227+
System.out.println("Image schema:\n" + actualSchema.toString(true));
228+
229+
// THEN
230+
assertThat(actualSchema.getType()).isEqualTo(Schema.Type.UNION);
231+
assertThat(actualSchema.getTypes()).containsExactlyInAnyOrder(imageItselfSchema, jpegSchema, pngSchema);
232+
}
233+
234+
@Union({
235+
// Base class being explicitly in @Union led to StackOverflowError exception.
236+
Sport.class,
237+
Football.class, Basketball.class})
238+
@AvroNamespace(TEST_NAMESPACE) // @AvroNamespace makes it easier to create schema string representation
239+
private static class Sport {
240+
}
241+
242+
private static final String SPORT_ITSELF_SCHEMA_STR = "{\"type\":\"record\",\"name\":\"Sport\",\"namespace\":\"test\",\"fields\":[]}";
243+
244+
private static class Football extends Sport {
245+
}
246+
247+
private static class Basketball extends Sport {
248+
}
249+
250+
@Test
251+
public void base_class_explicitly_in_Union_annotation_test() throws IOException {
252+
// GIVEN
253+
final Schema sportItselfSchema = MAPPER.schemaFrom(SPORT_ITSELF_SCHEMA_STR).getAvroSchema();
254+
final Schema footballSchema = MAPPER.schemaFor(Football.class).getAvroSchema();
255+
final Schema basketballSchema = MAPPER.schemaFor(Basketball.class).getAvroSchema();
256+
257+
// WHEN
258+
Schema actualSchema = MAPPER.schemaFor(Sport.class).getAvroSchema();
259+
260+
System.out.println("Sport schema:\n" + actualSchema.toString(true));
261+
262+
// THEN
263+
assertThat(actualSchema.getType()).isEqualTo(Schema.Type.UNION);
264+
assertThat(actualSchema.getTypes()).containsExactlyInAnyOrder(sportItselfSchema, footballSchema, basketballSchema);
265+
}
266+
267+
@Union({
268+
// Interface being explicitly in @Union led to StackOverflowError exception.
269+
DocumentInterface.class,
270+
Word.class, Excel.class})
271+
private interface DocumentInterface {
272+
}
273+
274+
private static class Word implements DocumentInterface {
275+
}
276+
277+
private static class Excel implements DocumentInterface {
278+
}
279+
280+
@Test
281+
public void interface_explicitly_in_Union_annotation_test() throws IOException {
282+
// GIVEN
283+
final Schema wordSchema = MAPPER.schemaFor(Word.class).getAvroSchema();
284+
final Schema excelSchema = MAPPER.schemaFor(Excel.class).getAvroSchema();
285+
286+
// WHEN
287+
Schema actualSchema = MAPPER.schemaFor(DocumentInterface.class).getAvroSchema();
288+
289+
System.out.println("Document schema:\n" + actualSchema.toString(true));
290+
291+
// THEN
292+
assertThat(actualSchema.getType()).isEqualTo(Schema.Type.UNION);
293+
assertThat(actualSchema.getTypes()).containsExactlyInAnyOrder(wordSchema, excelSchema);
294+
}
295+
198296
}

0 commit comments

Comments
 (0)