From 03d344a443a603fe0b482a40f806f6bda17ad48a Mon Sep 17 00:00:00 2001 From: Michal Foksa Date: Tue, 13 May 2025 16:20:05 +0200 Subject: [PATCH 01/11] Record visitor includes concrete base class (base class is not abstract or interface) into union of types. --- .../dataformat/avro/schema/RecordVisitor.java | 74 +++++++-- .../PolymorphicTypeAnnotationsTest.java | 156 ++++++++++++++++++ 2 files changed, 213 insertions(+), 17 deletions(-) create mode 100644 avro/src/test/java/com/fasterxml/jackson/dataformat/avro/schema/PolymorphicTypeAnnotationsTest.java diff --git a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/schema/RecordVisitor.java b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/schema/RecordVisitor.java index 30a00dea2..4348e5121 100644 --- a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/schema/RecordVisitor.java +++ b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/schema/RecordVisitor.java @@ -31,6 +31,33 @@ public class RecordVisitor */ protected final boolean _overridden; + /** + * When Avro schema for this JavaType ({@code _type}) results in UNION of multiple Avro types, _typeSchema keeps track + * which Avro type in the UNION represents this JavaType ({@code _type}) so that fields of this JavaType can be set to the right Avro type by {@code builtAvroSchema()}. + * + * Example: + *
+     *   @JsonSubTypes({
+     *     @JsonSubTypes.Type(value = Apple.class),
+     *     @JsonSubTypes.Type(value = Pear.class) })
+     *   class Fruit {}
+     *
+     *   class Apple extends Fruit {}
+     *   class Orange extends Fruit {}
+     * 
+ * When _type = Fruit.class + * Then + * _avroSchema if Fruit.class is union of Fruit record, Apple record and Orange record schemas: [ + * { name: Fruit, type: record, field: [..] }, <--- _typeSchema points here + * { name: Apple, type: record, field: [..] }, + * { name: Orange, type: record, field: [..]} + * ] + * _typeSchema points to Fruit.class without subtypes record schema + * + * FIXME: When _thisSchema is not null, then _overridden must be true, therefore (_overridden == true) can be replaced with (_thisSchema != null), + * but it might be considered API change cause _overridden has protected access modifier. + */ + private Schema _typeSchema; protected Schema _avroSchema; protected List _fields = new ArrayList<>(); @@ -42,32 +69,45 @@ public RecordVisitor(SerializerProvider p, JavaType type, VisitorFormatWrapperIm _visitorWrapper = visitorWrapper; // Check if the schema for this record is overridden BeanDescription bean = getProvider().getConfig().introspectDirectClassAnnotations(_type); - List subTypes = getProvider().getAnnotationIntrospector().findSubtypes(bean.getClassInfo()); AvroSchema ann = bean.getClassInfo().getAnnotation(AvroSchema.class); if (ann != null) { _avroSchema = AvroSchemaHelper.parseJsonSchema(ann.value()); _overridden = true; - } else if (subTypes != null && !subTypes.isEmpty()) { - List unionSchemas = new ArrayList<>(); - try { - for (NamedType subType : subTypes) { - JsonSerializer ser = getProvider().findValueSerializer(subType.getType()); - VisitorFormatWrapperImpl visitor = _visitorWrapper.createChildWrapper(); - ser.acceptJsonFormatVisitor(visitor, getProvider().getTypeFactory().constructType(subType.getType())); - unionSchemas.add(visitor.getAvroSchema()); - } - _avroSchema = Schema.createUnion(unionSchemas); - _overridden = true; - } catch (JsonMappingException jme) { - throw new RuntimeException("Failed to build schema", jme); - } } else { - _avroSchema = AvroSchemaHelper.initializeRecordSchema(bean); + // If Avro schema for this _type results in UNION I want to know Avro type where to assign fields + _typeSchema = AvroSchemaHelper.initializeRecordSchema(bean); + _avroSchema = _typeSchema; _overridden = false; AvroMeta meta = bean.getClassInfo().getAnnotation(AvroMeta.class); if (meta != null) { _avroSchema.addProp(meta.key(), meta.value()); } + + List subTypes = getProvider().getAnnotationIntrospector().findSubtypes(bean.getClassInfo()); + if (subTypes != null && !subTypes.isEmpty()) { + List unionSchemas = new ArrayList<>(); + // Initialize with this schema + if (_type.isConcrete()) { + unionSchemas.add(_typeSchema); + } + try { + for (NamedType subType : subTypes) { + JsonSerializer ser = getProvider().findValueSerializer(subType.getType()); + VisitorFormatWrapperImpl visitor = _visitorWrapper.createChildWrapper(); + ser.acceptJsonFormatVisitor(visitor, getProvider().getTypeFactory().constructType(subType.getType())); + Schema subTypeSchema = visitor.getAvroSchema(); + // If subType schema is also UNION, include all its types into this union + if (subTypeSchema.getType() == Type.UNION) { + unionSchemas.addAll(subTypeSchema.getTypes()); + } else { + unionSchemas.add(subTypeSchema); + } + } + _avroSchema = Schema.createUnion(unionSchemas); + } catch (JsonMappingException jme) { + throw new RuntimeException("Failed to build schema", jme); + } + } } _visitorWrapper.getSchemas().addSchema(type, _avroSchema); } @@ -76,7 +116,7 @@ public RecordVisitor(SerializerProvider p, JavaType type, VisitorFormatWrapperIm public Schema builtAvroSchema() { if (!_overridden) { // Assumption now is that we are done, so let's assign fields - _avroSchema.setFields(_fields); + _typeSchema.setFields(_fields); } return _avroSchema; } diff --git a/avro/src/test/java/com/fasterxml/jackson/dataformat/avro/schema/PolymorphicTypeAnnotationsTest.java b/avro/src/test/java/com/fasterxml/jackson/dataformat/avro/schema/PolymorphicTypeAnnotationsTest.java new file mode 100644 index 000000000..c7f0239ee --- /dev/null +++ b/avro/src/test/java/com/fasterxml/jackson/dataformat/avro/schema/PolymorphicTypeAnnotationsTest.java @@ -0,0 +1,156 @@ +package com.fasterxml.jackson.dataformat.avro.schema; + +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.dataformat.avro.AvroMapper; +import com.fasterxml.jackson.dataformat.avro.annotation.AvroNamespace; +import org.apache.avro.Schema; +import org.junit.jupiter.api.Test; + +import java.io.IOException; + +import static org.assertj.core.api.Assertions.assertThat; + +public class PolymorphicTypeAnnotationsTest { + + private static final AvroMapper MAPPER = AvroMapper.builder().build(); + // it is easier maintain string schema representation when namespace is constant, rather than being inferend from this class package name + private static final String TEST_NAMESPACE = "test"; + + @JsonSubTypes({ + @JsonSubTypes.Type(value = Cat.class), + @JsonSubTypes.Type(value = Dog.class), + }) + private interface AnimalInterface { + } + + private static abstract class AbstractMammal implements AnimalInterface { + public int legs; + } + + static class Cat extends AbstractMammal { + public String color; + } + + static class Dog extends AbstractMammal { + public int size; + } + + @Test + public void subclasses_of_interface_test() throws JsonMappingException { + // GIVEN + final Schema catSchema = MAPPER.schemaFor(Cat.class).getAvroSchema(); + final Schema dogSchema = MAPPER.schemaFor(Dog.class).getAvroSchema(); + + // WHEN + Schema actualSchema = MAPPER.schemaFor(AnimalInterface.class).getAvroSchema(); + + System.out.println("Animal schema:\n" + actualSchema.toString(true)); + + // THEN + assertThat(actualSchema.getType()).isEqualTo(Schema.Type.UNION); + // Because AnimalInterface is interface and AbstractMammal is abstract, they are not expected to be among types in union + assertThat(actualSchema.getTypes()).containsExactlyInAnyOrder(catSchema, dogSchema); + } + + @JsonSubTypes({ + @JsonSubTypes.Type(value = Apple.class), + @JsonSubTypes.Type(value = Pear.class), + }) + @AvroNamespace(TEST_NAMESPACE) // @AvroNamespace makes it easier to create schema string representation + static class Fruit { + public boolean eatable; + } + + private static final String FRUIT_ITSELF_SCHEMA_STR = "{\"type\":\"record\",\"name\":\"Fruit\",\"namespace\":\"test\",\"fields\":[{\"name\":\"eatable\",\"type\":\"boolean\"}]}"; + + static class Apple extends Fruit { + public String color; + } + + static class Pear extends Fruit { + public int seeds; + } + + @Test + public void subclasses_of_concrete_class_test() throws IOException { + // GIVEN + final Schema fruitItselfSchema = MAPPER.schemaFrom(FRUIT_ITSELF_SCHEMA_STR).getAvroSchema(); + final Schema appleSchema = MAPPER.schemaFor(Apple.class).getAvroSchema(); + final Schema pearSchema = MAPPER.schemaFor(Pear.class).getAvroSchema(); + + // WHEN + Schema actualSchema = MAPPER.schemaFor(Fruit.class).getAvroSchema(); + + System.out.println("Fruit schema:\n" + actualSchema.toString(true)); + + // THEN + assertThat(actualSchema.getType()).isEqualTo(Schema.Type.UNION); + assertThat(actualSchema.getTypes()).containsExactlyInAnyOrder(fruitItselfSchema, appleSchema, pearSchema); + } + + @JsonSubTypes({ + @JsonSubTypes.Type(value = LandVehicle.class), + @JsonSubTypes.Type(value = AbstractWaterVehicle.class), + }) + @AvroNamespace(TEST_NAMESPACE) + private static class Vehicle { + } + + private static final String VEHICLE_ITSELF_SCHEMA_STR = "{\"type\":\"record\",\"name\":\"Vehicle\",\"namespace\":\"test\",\"fields\":[]}"; + + @JsonSubTypes({ + @JsonSubTypes.Type(value = Car.class), + @JsonSubTypes.Type(value = MotorCycle.class), + }) + @AvroNamespace(TEST_NAMESPACE) + private static class LandVehicle extends Vehicle { + } + + private static final String LAND_VEHICLE_ITSELF_SCHEMA_STR = "{\"type\":\"record\",\"name\":\"LandVehicle\",\"namespace\":\"test\",\"fields\":[]}"; + + private static class Car extends LandVehicle { + } + + private static class MotorCycle extends LandVehicle { + } + + @JsonSubTypes({ + @JsonSubTypes.Type(value = Boat.class), + @JsonSubTypes.Type(value = Submarine.class), + }) + private static abstract class AbstractWaterVehicle extends Vehicle { + public int propellers; + } + + private static class Boat extends AbstractWaterVehicle { + } + + private static class Submarine extends AbstractWaterVehicle { + } + + @Test + public void subclasses_of_subclasses_test() throws IOException { + // GIVEN + final Schema vehicleItselfSchema = MAPPER.schemaFrom(VEHICLE_ITSELF_SCHEMA_STR).getAvroSchema(); + final Schema landVehicleItselfSchema = MAPPER.schemaFrom(LAND_VEHICLE_ITSELF_SCHEMA_STR).getAvroSchema(); + final Schema carSchema = MAPPER.schemaFor(Car.class).getAvroSchema(); + final Schema motorCycleSchema = MAPPER.schemaFor(MotorCycle.class).getAvroSchema(); + final Schema boatSchema = MAPPER.schemaFor(Boat.class).getAvroSchema(); + final Schema submarineSchema = MAPPER.schemaFor(Submarine.class).getAvroSchema(); + + // WHEN + Schema actualSchema = MAPPER.schemaFor(Vehicle.class).getAvroSchema(); + + System.out.println("Vehicle schema:\n" + actualSchema.toString(true)); + + // THEN + assertThat(actualSchema.getType()).isEqualTo(Schema.Type.UNION); + assertThat(actualSchema.getTypes()).containsExactlyInAnyOrder( + vehicleItselfSchema, + landVehicleItselfSchema, carSchema, motorCycleSchema, + // AbstractWaterVehicle is not here, because it is abstract + boatSchema, submarineSchema); + } + +} From 46cbe125fcd489fc7193824cecd8bbfb6a40f254 Mon Sep 17 00:00:00 2001 From: Michal Foksa Date: Thu, 15 May 2025 22:59:36 +0200 Subject: [PATCH 02/11] Add subType schema into this union, unless it is already there. --- .../dataformat/avro/schema/RecordVisitor.java | 45 ++++++++++++++++--- 1 file changed, 38 insertions(+), 7 deletions(-) diff --git a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/schema/RecordVisitor.java b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/schema/RecordVisitor.java index 4348e5121..e7783d829 100644 --- a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/schema/RecordVisitor.java +++ b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/schema/RecordVisitor.java @@ -85,25 +85,56 @@ public RecordVisitor(SerializerProvider p, JavaType type, VisitorFormatWrapperIm List subTypes = getProvider().getAnnotationIntrospector().findSubtypes(bean.getClassInfo()); if (subTypes != null && !subTypes.isEmpty()) { - List unionSchemas = new ArrayList<>(); - // Initialize with this schema - if (_type.isConcrete()) { - unionSchemas.add(_typeSchema); - } + + // At this point calculating hashCode for _typeSchema fails with NPE because RecordSchema.fields is NULL + // (see org.apache.avro.Schema.RecordSchema#computeHash). + // Therefore, _typeSchema must be added into union at very end, or unionSchemas must not be HashSet (or any + // other type calling hashCode() for equality check). + Set unionSchemas = new HashSet<>(); + // ArrayList unionSchemas = new ArrayList<>(); + + // IdentityHashMap is used because it is using reference-equality. + // Set unionSchemas = Collections.newSetFromMap(new IdentityHashMap<>()); + + // Initialize with this schema is + // if (_type.isConcrete()) { + // unionSchemas.add(_typeSchema); + // } + try { for (NamedType subType : subTypes) { JsonSerializer ser = getProvider().findValueSerializer(subType.getType()); VisitorFormatWrapperImpl visitor = _visitorWrapper.createChildWrapper(); ser.acceptJsonFormatVisitor(visitor, getProvider().getTypeFactory().constructType(subType.getType())); Schema subTypeSchema = visitor.getAvroSchema(); - // If subType schema is also UNION, include all its types into this union + // Add subType schema into this union, unless it is already there. + // When subType schema is itself a union, include all its types into this union if (subTypeSchema.getType() == Type.UNION) { +// subTypeSchema.getTypes().stream() +// .filter(unionElement -> !unionSchemas.contains(unionElement)) +// .forEach(unionSchemas::add); + // or +// for( Schema unionElement: subTypeSchema.getTypes()) { +// if (unionSchemas.contains(unionElement)) { +// continue; +// } +// unionSchemas.add(unionElement); +// } unionSchemas.addAll(subTypeSchema.getTypes()); } else { +// if (!unionSchemas.contains(subTypeSchema)) { +// unionSchemas.add(subTypeSchema); +// } unionSchemas.add(subTypeSchema); } } - _avroSchema = Schema.createUnion(unionSchemas); + + ArrayList unionList = new ArrayList<>(unionSchemas); + // add _type schema into union + if (_type.isConcrete()) { + unionList.add(_typeSchema); + } + _avroSchema = Schema.createUnion(unionList); } catch (JsonMappingException jme) { throw new RuntimeException("Failed to build schema", jme); } From 99311ae5c411f1b4ee261e89b33c3009fb1c794f Mon Sep 17 00:00:00 2001 From: Michal Foksa Date: Thu, 15 May 2025 23:30:30 +0200 Subject: [PATCH 03/11] Add subType schema into this union, unless it is already there. --- .../PolymorphicTypeAnnotationsTest.java | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/avro/src/test/java/com/fasterxml/jackson/dataformat/avro/schema/PolymorphicTypeAnnotationsTest.java b/avro/src/test/java/com/fasterxml/jackson/dataformat/avro/schema/PolymorphicTypeAnnotationsTest.java index c7f0239ee..a7c1cca7e 100644 --- a/avro/src/test/java/com/fasterxml/jackson/dataformat/avro/schema/PolymorphicTypeAnnotationsTest.java +++ b/avro/src/test/java/com/fasterxml/jackson/dataformat/avro/schema/PolymorphicTypeAnnotationsTest.java @@ -153,4 +153,46 @@ public void subclasses_of_subclasses_test() throws IOException { boatSchema, submarineSchema); } + // Helium is twice in subtypes hierarchy, once as ElementInterface subtype and second time as subtype + // of AbstractGas subtype. This situation may result in + // "Failed to generate `AvroSchema` for ...., problem: (AvroRuntimeException) Duplicate in union:com.fasterxml...PolymorphicTypeAnnotationsTest.Helium" + // error. + @JsonSubTypes({ + @JsonSubTypes.Type(value = AbstractGas.class), + @JsonSubTypes.Type(value = Helium.class), + }) + private interface ElementInterface { + } + + @JsonSubTypes({ + @JsonSubTypes.Type(value = Helium.class), + @JsonSubTypes.Type(value = Oxygen.class), + }) + static abstract class AbstractGas implements ElementInterface { + public int atomicMass; + } + + private static class Helium extends AbstractGas { + } + + private static class Oxygen extends AbstractGas { + } + + @Test + public void class_is_referenced_twice_in_hierarchy_test() throws JsonMappingException { + // GIVEN + final Schema heliumSchema = MAPPER.schemaFor(Helium.class).getAvroSchema(); + final Schema oxygenSchema = MAPPER.schemaFor(Oxygen.class).getAvroSchema(); + + // WHEN + Schema actualSchema = MAPPER.schemaFor(ElementInterface.class).getAvroSchema(); + + System.out.println("ElementInterface schema:\n" + actualSchema.toString(true)); + + // THEN + assertThat(actualSchema.getType()).isEqualTo(Schema.Type.UNION); + // ElementInterface and AbstractGas are not concrete classes they are not expected to be among types in union + assertThat(actualSchema.getTypes()).containsExactlyInAnyOrder(heliumSchema, oxygenSchema); + } + } From 8f09acc182c2461d02c3f22dc3f679869c0acda0 Mon Sep 17 00:00:00 2001 From: Michal Foksa Date: Sat, 17 May 2025 07:40:38 +0200 Subject: [PATCH 04/11] comments --- .../jackson/dataformat/avro/schema/RecordVisitor.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/schema/RecordVisitor.java b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/schema/RecordVisitor.java index e7783d829..ffcc9bf51 100644 --- a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/schema/RecordVisitor.java +++ b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/schema/RecordVisitor.java @@ -48,13 +48,13 @@ public class RecordVisitor * When _type = Fruit.class * Then * _avroSchema if Fruit.class is union of Fruit record, Apple record and Orange record schemas: [ - * { name: Fruit, type: record, field: [..] }, <--- _typeSchema points here - * { name: Apple, type: record, field: [..] }, - * { name: Orange, type: record, field: [..]} + * { name: Fruit, type: record, fields: [..] }, <--- _typeSchema points here + * { name: Apple, type: record, fields: [..] }, + * { name: Orange, type: record, fields: [..]} * ] * _typeSchema points to Fruit.class without subtypes record schema * - * FIXME: When _thisSchema is not null, then _overridden must be true, therefore (_overridden == true) can be replaced with (_thisSchema != null), + * FIXME: When _typeSchema is not null, then _overridden must be true, therefore (_overridden == true) can be replaced with (_typeSchema != null), * but it might be considered API change cause _overridden has protected access modifier. */ private Schema _typeSchema; @@ -108,7 +108,7 @@ public RecordVisitor(SerializerProvider p, JavaType type, VisitorFormatWrapperIm ser.acceptJsonFormatVisitor(visitor, getProvider().getTypeFactory().constructType(subType.getType())); Schema subTypeSchema = visitor.getAvroSchema(); // Add subType schema into this union, unless it is already there. - // When subType schema is itself a union, include all its types into this union + // When subType schema is union itself, include each its type into this union if not there already if (subTypeSchema.getType() == Type.UNION) { // subTypeSchema.getTypes().stream() // .filter(unionElement -> !unionSchemas.contains(unionElement)) From c540e07070eb867c0cf17c445d6d17632903ab73 Mon Sep 17 00:00:00 2001 From: Michal Foksa Date: Sat, 17 May 2025 08:34:35 +0200 Subject: [PATCH 05/11] Using reference-equality Set for unionSchemas. It makes for simples code. --- .../dataformat/avro/schema/RecordVisitor.java | 44 ++++--------------- 1 file changed, 9 insertions(+), 35 deletions(-) diff --git a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/schema/RecordVisitor.java b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/schema/RecordVisitor.java index ffcc9bf51..4d062d555 100644 --- a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/schema/RecordVisitor.java +++ b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/schema/RecordVisitor.java @@ -85,56 +85,30 @@ public RecordVisitor(SerializerProvider p, JavaType type, VisitorFormatWrapperIm List subTypes = getProvider().getAnnotationIntrospector().findSubtypes(bean.getClassInfo()); if (subTypes != null && !subTypes.isEmpty()) { - // At this point calculating hashCode for _typeSchema fails with NPE because RecordSchema.fields is NULL - // (see org.apache.avro.Schema.RecordSchema#computeHash). - // Therefore, _typeSchema must be added into union at very end, or unionSchemas must not be HashSet (or any - // other type calling hashCode() for equality check). - Set unionSchemas = new HashSet<>(); - // ArrayList unionSchemas = new ArrayList<>(); - + // see org.apache.avro.Schema.RecordSchema#computeHash. + // Therefore, unionSchemas must not be HashSet (or any other type using hashCode() for equality check). // IdentityHashMap is used because it is using reference-equality. - // Set unionSchemas = Collections.newSetFromMap(new IdentityHashMap<>()); - - // Initialize with this schema is - // if (_type.isConcrete()) { - // unionSchemas.add(_typeSchema); - // } - + Set unionSchemas = Collections.newSetFromMap(new IdentityHashMap<>()); + // Initialize with this schema + if (_type.isConcrete()) { + unionSchemas.add(_typeSchema); + } try { for (NamedType subType : subTypes) { JsonSerializer ser = getProvider().findValueSerializer(subType.getType()); VisitorFormatWrapperImpl visitor = _visitorWrapper.createChildWrapper(); ser.acceptJsonFormatVisitor(visitor, getProvider().getTypeFactory().constructType(subType.getType())); - Schema subTypeSchema = visitor.getAvroSchema(); // Add subType schema into this union, unless it is already there. + Schema subTypeSchema = visitor.getAvroSchema(); // When subType schema is union itself, include each its type into this union if not there already if (subTypeSchema.getType() == Type.UNION) { -// subTypeSchema.getTypes().stream() -// .filter(unionElement -> !unionSchemas.contains(unionElement)) -// .forEach(unionSchemas::add); - // or -// for( Schema unionElement: subTypeSchema.getTypes()) { -// if (unionSchemas.contains(unionElement)) { -// continue; -// } -// unionSchemas.add(unionElement); -// } unionSchemas.addAll(subTypeSchema.getTypes()); } else { -// if (!unionSchemas.contains(subTypeSchema)) { -// unionSchemas.add(subTypeSchema); -// } unionSchemas.add(subTypeSchema); } } - - ArrayList unionList = new ArrayList<>(unionSchemas); - // add _type schema into union - if (_type.isConcrete()) { - unionList.add(_typeSchema); - } - _avroSchema = Schema.createUnion(unionList); + _avroSchema = Schema.createUnion(new ArrayList<>(unionSchemas)); } catch (JsonMappingException jme) { throw new RuntimeException("Failed to build schema", jme); } From 88fa1dc5245475b5363e29a30445215f92b365a9 Mon Sep 17 00:00:00 2001 From: Michal Foksa Date: Sat, 17 May 2025 08:49:51 +0200 Subject: [PATCH 06/11] Making classes used in test private so that they do not soak outside. --- .../avro/schema/PolymorphicTypeAnnotationsTest.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/avro/src/test/java/com/fasterxml/jackson/dataformat/avro/schema/PolymorphicTypeAnnotationsTest.java b/avro/src/test/java/com/fasterxml/jackson/dataformat/avro/schema/PolymorphicTypeAnnotationsTest.java index a7c1cca7e..d1b700980 100644 --- a/avro/src/test/java/com/fasterxml/jackson/dataformat/avro/schema/PolymorphicTypeAnnotationsTest.java +++ b/avro/src/test/java/com/fasterxml/jackson/dataformat/avro/schema/PolymorphicTypeAnnotationsTest.java @@ -14,7 +14,7 @@ public class PolymorphicTypeAnnotationsTest { private static final AvroMapper MAPPER = AvroMapper.builder().build(); - // it is easier maintain string schema representation when namespace is constant, rather than being inferend from this class package name + // it is easier maintain string schema representation when namespace is constant, rather than being inferred from this class package name private static final String TEST_NAMESPACE = "test"; @JsonSubTypes({ @@ -28,11 +28,11 @@ private static abstract class AbstractMammal implements AnimalInterface { public int legs; } - static class Cat extends AbstractMammal { + private static class Cat extends AbstractMammal { public String color; } - static class Dog extends AbstractMammal { + private static class Dog extends AbstractMammal { public int size; } @@ -58,17 +58,17 @@ public void subclasses_of_interface_test() throws JsonMappingException { @JsonSubTypes.Type(value = Pear.class), }) @AvroNamespace(TEST_NAMESPACE) // @AvroNamespace makes it easier to create schema string representation - static class Fruit { + private static class Fruit { public boolean eatable; } private static final String FRUIT_ITSELF_SCHEMA_STR = "{\"type\":\"record\",\"name\":\"Fruit\",\"namespace\":\"test\",\"fields\":[{\"name\":\"eatable\",\"type\":\"boolean\"}]}"; - static class Apple extends Fruit { + private static class Apple extends Fruit { public String color; } - static class Pear extends Fruit { + private static class Pear extends Fruit { public int seeds; } From 792e0d2dbe69c2ed934f06c4a3dcc41b80aa0f92 Mon Sep 17 00:00:00 2001 From: Michal Foksa Date: Mon, 19 May 2025 20:03:47 +0200 Subject: [PATCH 07/11] Prevention of StackOverflowError exception when base class is explicitly in @JsonSubTypes or @Union annotations. --- .../dataformat/avro/schema/RecordVisitor.java | 9 ++ .../PolymorphicTypeAnnotationsTest.java | 98 +++++++++++++++++++ 2 files changed, 107 insertions(+) diff --git a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/schema/RecordVisitor.java b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/schema/RecordVisitor.java index 4d062d555..82da9b0b3 100644 --- a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/schema/RecordVisitor.java +++ b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/schema/RecordVisitor.java @@ -85,17 +85,26 @@ public RecordVisitor(SerializerProvider p, JavaType type, VisitorFormatWrapperIm List subTypes = getProvider().getAnnotationIntrospector().findSubtypes(bean.getClassInfo()); if (subTypes != null && !subTypes.isEmpty()) { + // alreadySeenClasses prevents subType processing in endless loop + Set> alreadySeenClasses = new HashSet<>(); + alreadySeenClasses.add(_type.getRawClass()); + // At this point calculating hashCode for _typeSchema fails with NPE because RecordSchema.fields is NULL // see org.apache.avro.Schema.RecordSchema#computeHash. // Therefore, unionSchemas must not be HashSet (or any other type using hashCode() for equality check). + // Set ensures that each subType schema is once in resulting union. // IdentityHashMap is used because it is using reference-equality. Set unionSchemas = Collections.newSetFromMap(new IdentityHashMap<>()); // Initialize with this schema if (_type.isConcrete()) { unionSchemas.add(_typeSchema); } + try { for (NamedType subType : subTypes) { + if (!alreadySeenClasses.add(subType.getType())) { + continue; + } JsonSerializer ser = getProvider().findValueSerializer(subType.getType()); VisitorFormatWrapperImpl visitor = _visitorWrapper.createChildWrapper(); ser.acceptJsonFormatVisitor(visitor, getProvider().getTypeFactory().constructType(subType.getType())); diff --git a/avro/src/test/java/com/fasterxml/jackson/dataformat/avro/schema/PolymorphicTypeAnnotationsTest.java b/avro/src/test/java/com/fasterxml/jackson/dataformat/avro/schema/PolymorphicTypeAnnotationsTest.java index d1b700980..12a61a1b8 100644 --- a/avro/src/test/java/com/fasterxml/jackson/dataformat/avro/schema/PolymorphicTypeAnnotationsTest.java +++ b/avro/src/test/java/com/fasterxml/jackson/dataformat/avro/schema/PolymorphicTypeAnnotationsTest.java @@ -5,6 +5,7 @@ import com.fasterxml.jackson.dataformat.avro.AvroMapper; import com.fasterxml.jackson.dataformat.avro.annotation.AvroNamespace; import org.apache.avro.Schema; +import org.apache.avro.reflect.Union; import org.junit.jupiter.api.Test; import java.io.IOException; @@ -195,4 +196,101 @@ public void class_is_referenced_twice_in_hierarchy_test() throws JsonMappingExce assertThat(actualSchema.getTypes()).containsExactlyInAnyOrder(heliumSchema, oxygenSchema); } + @JsonSubTypes({ + // Base class being explicitly in @JsonSubTypes led to StackOverflowError exception. + @JsonSubTypes.Type(value = Image.class), + @JsonSubTypes.Type(value = Jpeg.class), + @JsonSubTypes.Type(value = Png.class), + }) + @AvroNamespace(TEST_NAMESPACE) // @AvroNamespace makes it easier to create schema string representation + private static class Image { + } + + private static final String IMAGE_ITSELF_SCHEMA_STR = "{\"type\":\"record\",\"name\":\"Image\",\"namespace\":\"test\",\"fields\":[]}"; + + private static class Jpeg extends Image { + } + + private static class Png extends Image { + } + + @Test + public void base_class_explicitly_in_JsonSubTypes_annotation_test() throws IOException { + // GIVEN + final Schema imageItselfSchema = MAPPER.schemaFrom(IMAGE_ITSELF_SCHEMA_STR).getAvroSchema(); + final Schema jpegSchema = MAPPER.schemaFor(Jpeg.class).getAvroSchema(); + final Schema pngSchema = MAPPER.schemaFor(Png.class).getAvroSchema(); + + // WHEN + Schema actualSchema = MAPPER.schemaFor(Image.class).getAvroSchema(); + + System.out.println("Image schema:\n" + actualSchema.toString(true)); + + // THEN + assertThat(actualSchema.getType()).isEqualTo(Schema.Type.UNION); + assertThat(actualSchema.getTypes()).containsExactlyInAnyOrder(imageItselfSchema, jpegSchema, pngSchema); + } + + @Union({ + // Base class being explicitly in @Union led to StackOverflowError exception. + Sport.class, + Football.class, Basketball.class}) + @AvroNamespace(TEST_NAMESPACE) // @AvroNamespace makes it easier to create schema string representation + private static class Sport { + } + + private static final String SPORT_ITSELF_SCHEMA_STR = "{\"type\":\"record\",\"name\":\"Sport\",\"namespace\":\"test\",\"fields\":[]}"; + + private static class Football extends Sport { + } + + private static class Basketball extends Sport { + } + + @Test + public void base_class_explicitly_in_Union_annotation_test() throws IOException { + // GIVEN + final Schema sportItselfSchema = MAPPER.schemaFrom(SPORT_ITSELF_SCHEMA_STR).getAvroSchema(); + final Schema footballSchema = MAPPER.schemaFor(Football.class).getAvroSchema(); + final Schema basketballSchema = MAPPER.schemaFor(Basketball.class).getAvroSchema(); + + // WHEN + Schema actualSchema = MAPPER.schemaFor(Sport.class).getAvroSchema(); + + System.out.println("Sport schema:\n" + actualSchema.toString(true)); + + // THEN + assertThat(actualSchema.getType()).isEqualTo(Schema.Type.UNION); + assertThat(actualSchema.getTypes()).containsExactlyInAnyOrder(sportItselfSchema, footballSchema, basketballSchema); + } + + @Union({ + // Interface being explicitly in @Union led to StackOverflowError exception. + DocumentInterface.class, + Word.class, Excel.class}) + private interface DocumentInterface { + } + + private static class Word implements DocumentInterface { + } + + private static class Excel implements DocumentInterface { + } + + @Test + public void interface_explicitly_in_Union_annotation_test() throws IOException { + // GIVEN + final Schema wordSchema = MAPPER.schemaFor(Word.class).getAvroSchema(); + final Schema excelSchema = MAPPER.schemaFor(Excel.class).getAvroSchema(); + + // WHEN + Schema actualSchema = MAPPER.schemaFor(DocumentInterface.class).getAvroSchema(); + + System.out.println("Document schema:\n" + actualSchema.toString(true)); + + // THEN + assertThat(actualSchema.getType()).isEqualTo(Schema.Type.UNION); + assertThat(actualSchema.getTypes()).containsExactlyInAnyOrder(wordSchema, excelSchema); + } + } From a13d0d6c70d33eab760172fefc3854801a9874bc Mon Sep 17 00:00:00 2001 From: Michal Foksa Date: Mon, 19 May 2025 22:28:38 +0200 Subject: [PATCH 08/11] test cases renamed --- .../avro/schema/PolymorphicTypeAnnotationsTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/avro/src/test/java/com/fasterxml/jackson/dataformat/avro/schema/PolymorphicTypeAnnotationsTest.java b/avro/src/test/java/com/fasterxml/jackson/dataformat/avro/schema/PolymorphicTypeAnnotationsTest.java index 12a61a1b8..3360d640d 100644 --- a/avro/src/test/java/com/fasterxml/jackson/dataformat/avro/schema/PolymorphicTypeAnnotationsTest.java +++ b/avro/src/test/java/com/fasterxml/jackson/dataformat/avro/schema/PolymorphicTypeAnnotationsTest.java @@ -74,7 +74,7 @@ private static class Pear extends Fruit { } @Test - public void subclasses_of_concrete_class_test() throws IOException { + public void jsonSubTypes_on_concrete_class_test() throws IOException { // GIVEN final Schema fruitItselfSchema = MAPPER.schemaFrom(FRUIT_ITSELF_SCHEMA_STR).getAvroSchema(); final Schema appleSchema = MAPPER.schemaFor(Apple.class).getAvroSchema(); @@ -131,7 +131,7 @@ private static class Submarine extends AbstractWaterVehicle { } @Test - public void subclasses_of_subclasses_test() throws IOException { + public void jsonSubTypes_of_jsonSubTypes_test() throws IOException { // GIVEN final Schema vehicleItselfSchema = MAPPER.schemaFrom(VEHICLE_ITSELF_SCHEMA_STR).getAvroSchema(); final Schema landVehicleItselfSchema = MAPPER.schemaFrom(LAND_VEHICLE_ITSELF_SCHEMA_STR).getAvroSchema(); From 67c6042eed1d0c30bc8fdabb6712139a6f7a0b08 Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Mon, 19 May 2025 19:34:24 -0700 Subject: [PATCH 09/11] Add release notes --- release-notes/CREDITS-2.x | 9 +++++++++ release-notes/VERSION-2.x | 6 ++++++ 2 files changed, 15 insertions(+) diff --git a/release-notes/CREDITS-2.x b/release-notes/CREDITS-2.x index 8dec9da09..b94303190 100644 --- a/release-notes/CREDITS-2.x +++ b/release-notes/CREDITS-2.x @@ -232,6 +232,9 @@ Michal Foksa (MichalFoksa@github) (2.19.0) * Contributed #536: (avro) Add Logical Type support for `java.util.UUID` (2.19.0) +* Contributed fix for #589: AvroSchema: Does not include base class for records + with subclasses + (2.19.1) Hunter Herman (hherman1@github) @@ -385,3 +388,9 @@ Manuel Sugawara (@sugmanue) Josh Curry (@seadbrane) * Reported, contributed fix for #571: Unable to deserialize a pojo with IonStruct (2.19.0) + +Rafael Winterhalter (@raphw) + * Reported #589: AvroSchema: Does not include base class for records + with subclasses + (2.19.1) + diff --git a/release-notes/VERSION-2.x b/release-notes/VERSION-2.x index 033fd9423..bae9b68aa 100644 --- a/release-notes/VERSION-2.x +++ b/release-notes/VERSION-2.x @@ -18,6 +18,12 @@ Active maintainers: - +2.19.1 (not yet released) + +#589: AvroSchema: Does not include base class for records with subclasses + (reported by Rafael W) + (fix contributed by Michal F) + 2.19.0 (24-Apr-2025) #300: (smile) Floats are encoded with sign extension while doubles without From 41d3060fc3e29e2fdc4f2857e0a61316d74051ca Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Mon, 19 May 2025 20:24:20 -0700 Subject: [PATCH 10/11] Warnings removal, cosmetic changes --- .../dataformat/avro/schema/RecordVisitor.java | 31 ++++--- .../PolymorphicTypeAnnotationsTest.java | 82 +++++++++---------- 2 files changed, 60 insertions(+), 53 deletions(-) diff --git a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/schema/RecordVisitor.java b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/schema/RecordVisitor.java index 82da9b0b3..11a93df5f 100644 --- a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/schema/RecordVisitor.java +++ b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/schema/RecordVisitor.java @@ -26,15 +26,16 @@ public class RecordVisitor protected final VisitorFormatWrapperImpl _visitorWrapper; /** - * Tracks if the schema for this record has been overridden (by an annotation or other means), and calls to the {@code property} and - * {@code optionalProperty} methods should be ignored. + * Tracks if the schema for this record has been overridden (by an annotation or other means), + * and calls to the {@code property} and {@code optionalProperty} methods should be ignored. */ protected final boolean _overridden; /** - * When Avro schema for this JavaType ({@code _type}) results in UNION of multiple Avro types, _typeSchema keeps track - * which Avro type in the UNION represents this JavaType ({@code _type}) so that fields of this JavaType can be set to the right Avro type by {@code builtAvroSchema()}. - * + * When Avro schema for this JavaType ({@code _type}) results in UNION of multiple Avro types, + * _typeSchema keeps track of which Avro type in the UNION represents this JavaType ({@code _type}) + * so that fields of this JavaType can be set to the right Avro type by {@code builtAvroSchema()}. + *
* Example: *
      *   @JsonSubTypes({
@@ -45,7 +46,7 @@ public class RecordVisitor
      *   class Apple extends Fruit {}
      *   class Orange extends Fruit {}
      * 
- * When _type = Fruit.class + * When {@code _type = Fruit.class} * Then * _avroSchema if Fruit.class is union of Fruit record, Apple record and Orange record schemas: [ * { name: Fruit, type: record, fields: [..] }, <--- _typeSchema points here @@ -56,10 +57,15 @@ public class RecordVisitor * * FIXME: When _typeSchema is not null, then _overridden must be true, therefore (_overridden == true) can be replaced with (_typeSchema != null), * but it might be considered API change cause _overridden has protected access modifier. + * + * @since 2.19.1 */ - private Schema _typeSchema; + private final Schema _typeSchema; + + // !!! 19-May-2025: TODO: make final in 2.20 protected Schema _avroSchema; + // !!! 19-May-2025: TODO: make final in 2.20 protected List _fields = new ArrayList<>(); public RecordVisitor(SerializerProvider p, JavaType type, VisitorFormatWrapperImpl visitorWrapper) @@ -73,6 +79,7 @@ public RecordVisitor(SerializerProvider p, JavaType type, VisitorFormatWrapperIm if (ann != null) { _avroSchema = AvroSchemaHelper.parseJsonSchema(ann.value()); _overridden = true; + _typeSchema = null; } else { // If Avro schema for this _type results in UNION I want to know Avro type where to assign fields _typeSchema = AvroSchemaHelper.initializeRecordSchema(bean); @@ -89,12 +96,14 @@ public RecordVisitor(SerializerProvider p, JavaType type, VisitorFormatWrapperIm Set> alreadySeenClasses = new HashSet<>(); alreadySeenClasses.add(_type.getRawClass()); - // At this point calculating hashCode for _typeSchema fails with NPE because RecordSchema.fields is NULL - // see org.apache.avro.Schema.RecordSchema#computeHash. - // Therefore, unionSchemas must not be HashSet (or any other type using hashCode() for equality check). + // At this point calculating hashCode for _typeSchema fails with + // NPE because RecordSchema.fields is NULL + // (see org.apache.avro.Schema.RecordSchema#computeHash). + // Therefore, unionSchemas must not be HashSet (or any other type + // using hashCode() for equality check). // Set ensures that each subType schema is once in resulting union. // IdentityHashMap is used because it is using reference-equality. - Set unionSchemas = Collections.newSetFromMap(new IdentityHashMap<>()); + final Set unionSchemas = Collections.newSetFromMap(new IdentityHashMap<>()); // Initialize with this schema if (_type.isConcrete()) { unionSchemas.add(_typeSchema); diff --git a/avro/src/test/java/com/fasterxml/jackson/dataformat/avro/schema/PolymorphicTypeAnnotationsTest.java b/avro/src/test/java/com/fasterxml/jackson/dataformat/avro/schema/PolymorphicTypeAnnotationsTest.java index 3360d640d..f21675478 100644 --- a/avro/src/test/java/com/fasterxml/jackson/dataformat/avro/schema/PolymorphicTypeAnnotationsTest.java +++ b/avro/src/test/java/com/fasterxml/jackson/dataformat/avro/schema/PolymorphicTypeAnnotationsTest.java @@ -1,14 +1,13 @@ package com.fasterxml.jackson.dataformat.avro.schema; +import org.junit.jupiter.api.Test; + import com.fasterxml.jackson.annotation.JsonSubTypes; -import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.dataformat.avro.AvroMapper; import com.fasterxml.jackson.dataformat.avro.annotation.AvroNamespace; + import org.apache.avro.Schema; import org.apache.avro.reflect.Union; -import org.junit.jupiter.api.Test; - -import java.io.IOException; import static org.assertj.core.api.Assertions.assertThat; @@ -22,23 +21,23 @@ public class PolymorphicTypeAnnotationsTest { @JsonSubTypes.Type(value = Cat.class), @JsonSubTypes.Type(value = Dog.class), }) - private interface AnimalInterface { + interface AnimalInterface { } - private static abstract class AbstractMammal implements AnimalInterface { + static abstract class AbstractMammal implements AnimalInterface { public int legs; } - private static class Cat extends AbstractMammal { + static class Cat extends AbstractMammal { public String color; } - private static class Dog extends AbstractMammal { + static class Dog extends AbstractMammal { public int size; } @Test - public void subclasses_of_interface_test() throws JsonMappingException { + public void subclasses_of_interface_test() throws Exception { // GIVEN final Schema catSchema = MAPPER.schemaFor(Cat.class).getAvroSchema(); final Schema dogSchema = MAPPER.schemaFor(Dog.class).getAvroSchema(); @@ -46,7 +45,7 @@ public void subclasses_of_interface_test() throws JsonMappingException { // WHEN Schema actualSchema = MAPPER.schemaFor(AnimalInterface.class).getAvroSchema(); - System.out.println("Animal schema:\n" + actualSchema.toString(true)); + // System.out.println("Animal schema:\n" + actualSchema.toString(true)); // THEN assertThat(actualSchema.getType()).isEqualTo(Schema.Type.UNION); @@ -59,22 +58,22 @@ public void subclasses_of_interface_test() throws JsonMappingException { @JsonSubTypes.Type(value = Pear.class), }) @AvroNamespace(TEST_NAMESPACE) // @AvroNamespace makes it easier to create schema string representation - private static class Fruit { + static class Fruit { public boolean eatable; } private static final String FRUIT_ITSELF_SCHEMA_STR = "{\"type\":\"record\",\"name\":\"Fruit\",\"namespace\":\"test\",\"fields\":[{\"name\":\"eatable\",\"type\":\"boolean\"}]}"; - private static class Apple extends Fruit { + static class Apple extends Fruit { public String color; } - private static class Pear extends Fruit { + static class Pear extends Fruit { public int seeds; } @Test - public void jsonSubTypes_on_concrete_class_test() throws IOException { + public void jsonSubTypes_on_concrete_class_test() throws Exception { // GIVEN final Schema fruitItselfSchema = MAPPER.schemaFrom(FRUIT_ITSELF_SCHEMA_STR).getAvroSchema(); final Schema appleSchema = MAPPER.schemaFor(Apple.class).getAvroSchema(); @@ -83,7 +82,7 @@ public void jsonSubTypes_on_concrete_class_test() throws IOException { // WHEN Schema actualSchema = MAPPER.schemaFor(Fruit.class).getAvroSchema(); - System.out.println("Fruit schema:\n" + actualSchema.toString(true)); + // System.out.println("Fruit schema:\n" + actualSchema.toString(true)); // THEN assertThat(actualSchema.getType()).isEqualTo(Schema.Type.UNION); @@ -95,7 +94,7 @@ public void jsonSubTypes_on_concrete_class_test() throws IOException { @JsonSubTypes.Type(value = AbstractWaterVehicle.class), }) @AvroNamespace(TEST_NAMESPACE) - private static class Vehicle { + static class Vehicle { } private static final String VEHICLE_ITSELF_SCHEMA_STR = "{\"type\":\"record\",\"name\":\"Vehicle\",\"namespace\":\"test\",\"fields\":[]}"; @@ -105,33 +104,33 @@ private static class Vehicle { @JsonSubTypes.Type(value = MotorCycle.class), }) @AvroNamespace(TEST_NAMESPACE) - private static class LandVehicle extends Vehicle { + static class LandVehicle extends Vehicle { } private static final String LAND_VEHICLE_ITSELF_SCHEMA_STR = "{\"type\":\"record\",\"name\":\"LandVehicle\",\"namespace\":\"test\",\"fields\":[]}"; - private static class Car extends LandVehicle { + static class Car extends LandVehicle { } - private static class MotorCycle extends LandVehicle { + static class MotorCycle extends LandVehicle { } @JsonSubTypes({ @JsonSubTypes.Type(value = Boat.class), @JsonSubTypes.Type(value = Submarine.class), }) - private static abstract class AbstractWaterVehicle extends Vehicle { + static abstract class AbstractWaterVehicle extends Vehicle { public int propellers; } - private static class Boat extends AbstractWaterVehicle { + static class Boat extends AbstractWaterVehicle { } - private static class Submarine extends AbstractWaterVehicle { + static class Submarine extends AbstractWaterVehicle { } @Test - public void jsonSubTypes_of_jsonSubTypes_test() throws IOException { + public void jsonSubTypes_of_jsonSubTypes_test() throws Exception { // GIVEN final Schema vehicleItselfSchema = MAPPER.schemaFrom(VEHICLE_ITSELF_SCHEMA_STR).getAvroSchema(); final Schema landVehicleItselfSchema = MAPPER.schemaFrom(LAND_VEHICLE_ITSELF_SCHEMA_STR).getAvroSchema(); @@ -143,7 +142,7 @@ public void jsonSubTypes_of_jsonSubTypes_test() throws IOException { // WHEN Schema actualSchema = MAPPER.schemaFor(Vehicle.class).getAvroSchema(); - System.out.println("Vehicle schema:\n" + actualSchema.toString(true)); + // System.out.println("Vehicle schema:\n" + actualSchema.toString(true)); // THEN assertThat(actualSchema.getType()).isEqualTo(Schema.Type.UNION); @@ -180,7 +179,7 @@ private static class Oxygen extends AbstractGas { } @Test - public void class_is_referenced_twice_in_hierarchy_test() throws JsonMappingException { + public void class_is_referenced_twice_in_hierarchy_test() throws Exception { // GIVEN final Schema heliumSchema = MAPPER.schemaFor(Helium.class).getAvroSchema(); final Schema oxygenSchema = MAPPER.schemaFor(Oxygen.class).getAvroSchema(); @@ -188,7 +187,7 @@ public void class_is_referenced_twice_in_hierarchy_test() throws JsonMappingExce // WHEN Schema actualSchema = MAPPER.schemaFor(ElementInterface.class).getAvroSchema(); - System.out.println("ElementInterface schema:\n" + actualSchema.toString(true)); + // System.out.println("ElementInterface schema:\n" + actualSchema.toString(true)); // THEN assertThat(actualSchema.getType()).isEqualTo(Schema.Type.UNION); @@ -203,19 +202,19 @@ public void class_is_referenced_twice_in_hierarchy_test() throws JsonMappingExce @JsonSubTypes.Type(value = Png.class), }) @AvroNamespace(TEST_NAMESPACE) // @AvroNamespace makes it easier to create schema string representation - private static class Image { + static class Image { } private static final String IMAGE_ITSELF_SCHEMA_STR = "{\"type\":\"record\",\"name\":\"Image\",\"namespace\":\"test\",\"fields\":[]}"; - private static class Jpeg extends Image { + static class Jpeg extends Image { } - private static class Png extends Image { + static class Png extends Image { } @Test - public void base_class_explicitly_in_JsonSubTypes_annotation_test() throws IOException { + public void base_class_explicitly_in_JsonSubTypes_annotation_test() throws Exception { // GIVEN final Schema imageItselfSchema = MAPPER.schemaFrom(IMAGE_ITSELF_SCHEMA_STR).getAvroSchema(); final Schema jpegSchema = MAPPER.schemaFor(Jpeg.class).getAvroSchema(); @@ -224,7 +223,7 @@ public void base_class_explicitly_in_JsonSubTypes_annotation_test() throws IOExc // WHEN Schema actualSchema = MAPPER.schemaFor(Image.class).getAvroSchema(); - System.out.println("Image schema:\n" + actualSchema.toString(true)); + // System.out.println("Image schema:\n" + actualSchema.toString(true)); // THEN assertThat(actualSchema.getType()).isEqualTo(Schema.Type.UNION); @@ -236,19 +235,19 @@ public void base_class_explicitly_in_JsonSubTypes_annotation_test() throws IOExc Sport.class, Football.class, Basketball.class}) @AvroNamespace(TEST_NAMESPACE) // @AvroNamespace makes it easier to create schema string representation - private static class Sport { + static class Sport { } private static final String SPORT_ITSELF_SCHEMA_STR = "{\"type\":\"record\",\"name\":\"Sport\",\"namespace\":\"test\",\"fields\":[]}"; - private static class Football extends Sport { + static class Football extends Sport { } - private static class Basketball extends Sport { + static class Basketball extends Sport { } @Test - public void base_class_explicitly_in_Union_annotation_test() throws IOException { + public void base_class_explicitly_in_Union_annotation_test() throws Exception { // GIVEN final Schema sportItselfSchema = MAPPER.schemaFrom(SPORT_ITSELF_SCHEMA_STR).getAvroSchema(); final Schema footballSchema = MAPPER.schemaFor(Football.class).getAvroSchema(); @@ -257,7 +256,7 @@ public void base_class_explicitly_in_Union_annotation_test() throws IOException // WHEN Schema actualSchema = MAPPER.schemaFor(Sport.class).getAvroSchema(); - System.out.println("Sport schema:\n" + actualSchema.toString(true)); + //System.out.println("Sport schema:\n" + actualSchema.toString(true)); // THEN assertThat(actualSchema.getType()).isEqualTo(Schema.Type.UNION); @@ -268,17 +267,17 @@ public void base_class_explicitly_in_Union_annotation_test() throws IOException // Interface being explicitly in @Union led to StackOverflowError exception. DocumentInterface.class, Word.class, Excel.class}) - private interface DocumentInterface { + interface DocumentInterface { } - private static class Word implements DocumentInterface { + static class Word implements DocumentInterface { } - private static class Excel implements DocumentInterface { + static class Excel implements DocumentInterface { } @Test - public void interface_explicitly_in_Union_annotation_test() throws IOException { + public void interface_explicitly_in_Union_annotation_test() throws Exception { // GIVEN final Schema wordSchema = MAPPER.schemaFor(Word.class).getAvroSchema(); final Schema excelSchema = MAPPER.schemaFor(Excel.class).getAvroSchema(); @@ -286,11 +285,10 @@ public void interface_explicitly_in_Union_annotation_test() throws IOException { // WHEN Schema actualSchema = MAPPER.schemaFor(DocumentInterface.class).getAvroSchema(); - System.out.println("Document schema:\n" + actualSchema.toString(true)); + //System.out.println("Document schema:\n" + actualSchema.toString(true)); // THEN assertThat(actualSchema.getType()).isEqualTo(Schema.Type.UNION); assertThat(actualSchema.getTypes()).containsExactlyInAnyOrder(wordSchema, excelSchema); } - } From 31ac1b70393b495901a82418239e71484b80f21e Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Mon, 19 May 2025 20:37:54 -0700 Subject: [PATCH 11/11] Minor tweaking to reduce diff, use Jackson's RuntimeException --- .../jackson/dataformat/avro/schema/RecordVisitor.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/schema/RecordVisitor.java b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/schema/RecordVisitor.java index 11a93df5f..a28cebc37 100644 --- a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/schema/RecordVisitor.java +++ b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/schema/RecordVisitor.java @@ -82,8 +82,8 @@ public RecordVisitor(SerializerProvider p, JavaType type, VisitorFormatWrapperIm _typeSchema = null; } else { // If Avro schema for this _type results in UNION I want to know Avro type where to assign fields - _typeSchema = AvroSchemaHelper.initializeRecordSchema(bean); - _avroSchema = _typeSchema; + _avroSchema = AvroSchemaHelper.initializeRecordSchema(bean); + _typeSchema = _avroSchema; _overridden = false; AvroMeta meta = bean.getClassInfo().getAnnotation(AvroMeta.class); if (meta != null) { @@ -128,7 +128,7 @@ public RecordVisitor(SerializerProvider p, JavaType type, VisitorFormatWrapperIm } _avroSchema = Schema.createUnion(new ArrayList<>(unionSchemas)); } catch (JsonMappingException jme) { - throw new RuntimeException("Failed to build schema", jme); + throw new RuntimeJsonMappingException("Failed to build schema", jme); } } }