Created
January 22, 2024 16:12
-
-
Save Chuckame/0e2b37e3cd09e1e525bacfbd4f9d028e to your computer and use it in GitHub Desktop.
few classes for having an avro ObjectMapper (AvroMapper, the avro format for jackson) with native nullability using NotNull annotations, and excluding discriminator field using native JsonSubTypeInfo
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import com.fasterxml.jackson.core.Version; | |
import com.fasterxml.jackson.databind.AnnotationIntrospector; | |
import com.fasterxml.jackson.databind.introspect.Annotated; | |
import com.fasterxml.jackson.databind.introspect.AnnotatedMember; | |
import com.fasterxml.jackson.databind.module.SimpleModule; | |
import java.lang.annotation.Annotation; | |
/** | |
* Also mark as non-required fields having other NotNull annotations. | |
* <p> | |
* If a collection field is marked required, then the default value is set to an empty array. | |
* <p> | |
* If any field is marked non-required, then the default value is set to literal `null`. | |
*/ | |
public class AvroNullSafetyAnnotationsModule extends SimpleModule { | |
private final Class<? extends Annotation>[] notNullAnnotations; | |
public AvroNullSafetyAnnotationsModule() { | |
this(new Class[]{jakarta.validation.constraints.NotNull.class, org.jetbrains.annotations.NotNull.class, jakarta.annotation.Nonnull.class, org.springframework.lang.NonNull.class}); | |
} | |
public AvroNullSafetyAnnotationsModule(Class<? extends Annotation>[] notNullAnnotations) { | |
this.notNullAnnotations = notNullAnnotations; | |
} | |
@Override | |
public void setupModule(SetupContext context) { | |
context.appendAnnotationIntrospector(new NullSafetyAnnotationIntrospector()); | |
} | |
private class NullSafetyAnnotationIntrospector extends AnnotationIntrospector { | |
@Override | |
public Boolean hasRequiredMarker(AnnotatedMember m) { | |
return isRequired(m) ? true : null; | |
} | |
private boolean isRequired(Annotated ann) { | |
return ann.hasOneOf(notNullAnnotations); | |
} | |
@Override | |
public String findPropertyDefaultValue(Annotated ann) { | |
if (!isRequired(ann)) { | |
return "null"; | |
} | |
if (ann.getType().isCollectionLikeType() && !ann.getType().isMapLikeType()) { | |
return "[]"; | |
} | |
return null; | |
} | |
@Override | |
public Version version() { | |
return Version.unknownVersion(); | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import com.fasterxml.jackson.databind.JsonNode; | |
import com.fasterxml.jackson.databind.MapperFeature; | |
import com.fasterxml.jackson.databind.ObjectMapper; | |
import com.fasterxml.jackson.databind.node.ContainerNode; | |
import com.fasterxml.jackson.databind.node.ObjectNode; | |
import com.fasterxml.jackson.dataformat.avro.AvroMapper; | |
import com.fasterxml.jackson.dataformat.avro.jsr310.AvroJavaTimeModule; | |
import com.fasterxml.jackson.dataformat.avro.schema.AvroSchemaGenerator; | |
import com.fasterxml.jackson.dataformat.avro.schema.AvroSchemaHelper; | |
import org.junit.jupiter.api.Test; | |
import java.io.IOException; | |
import java.nio.file.Files; | |
import java.nio.file.Paths; | |
class AvroSchemaGenerator { | |
private final AvroMapper avroMapper = IgnoreTypeDiscriminatorPropsModule.wrap(AvroMapper.builder() | |
.disable(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY) | |
.addModule(new AvroJavaTimeModule()) | |
.addModule(new AvroNullSafetyAnnotationsModule()) | |
.build()); | |
@Test | |
public void generateAvroSchema() throws IOException { | |
createAvroSchemaFromClass(MyClass.class, "<target namespace>"); | |
} | |
private String createAvroSchemaFromClass(Class<?> clazz, String packageOverride) throws IOException { | |
AvroSchemaGenerator gen = new AvroSchemaGenerator(); | |
gen.enableLogicalTypes(); | |
avroMapper.acceptJsonFormatVisitor(clazz, gen); | |
String avroSchemaInJSON = gen.getGeneratedSchema().getAvroSchema().toString(true).replace(clazz.getPackageName(), packageOverride); | |
var avroJsonNode = new ObjectMapper().readTree(avroSchemaInJSON); | |
removeFieldRecursively(avroJsonNode); | |
return avroJsonNode.toPrettyString(); | |
} | |
private static void removeFieldRecursively(JsonNode node) { | |
if (node instanceof ObjectNode objectNode) { | |
objectNode.remove(AvroSchemaHelper.AVRO_SCHEMA_PROP_CLASS); | |
} | |
if (node instanceof ContainerNode<?> containerNode) { | |
containerNode.forEach(AvroSchemaGeneratorTest::removeFieldRecursively); | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import com.fasterxml.jackson.annotation.JsonTypeInfo; | |
import com.fasterxml.jackson.core.Version; | |
import com.fasterxml.jackson.databind.AnnotationIntrospector; | |
import com.fasterxml.jackson.databind.ObjectMapper; | |
import com.fasterxml.jackson.databind.SerializationConfig; | |
import com.fasterxml.jackson.databind.introspect.AnnotatedMember; | |
import com.fasterxml.jackson.databind.module.SimpleModule; | |
import com.fasterxml.jackson.databind.type.TypeFactory; | |
/** | |
* Ignore type discriminator fields using {@link JsonTypeInfo} annotated on classes and fields. | |
*/ | |
public class IgnoreTypeDiscriminatorPropsModule extends SimpleModule { | |
private final SerializationConfig config; | |
public IgnoreTypeDiscriminatorPropsModule(SerializationConfig config) { | |
this.config = config; | |
} | |
public static <T extends ObjectMapper> T wrap(T mapper) { | |
ObjectMapper copied = mapper.copy();//prevent infinite loop because the added annotation introspector will call itself otherwise | |
mapper.registerModule(new IgnoreTypeDiscriminatorPropsModule(copied.getSerializationConfig())); | |
return mapper; | |
} | |
@Override | |
public void setupModule(SetupContext context) { | |
context.appendAnnotationIntrospector(new IgnoreTypeDiscriminatorPropsAnnotationIntrospector(context.getTypeFactory())); | |
} | |
private class IgnoreTypeDiscriminatorPropsAnnotationIntrospector extends AnnotationIntrospector { | |
private final TypeFactory typeFactory; | |
private IgnoreTypeDiscriminatorPropsAnnotationIntrospector(TypeFactory typeFactory) { | |
this.typeFactory = typeFactory; | |
} | |
@Override | |
public boolean hasIgnoreMarker(AnnotatedMember m) { | |
return isTypeDiscriminatorInnerProperty(m) || isTypeDiscriminatorOuterProperty(m); | |
} | |
private boolean isTypeDiscriminatorInnerProperty(AnnotatedMember m) { | |
var d = m.getDeclaringClass().getAnnotation(JsonTypeInfo.class); | |
if (d != null) { | |
return isInnerProperty(d) && isTypeDiscriminatorPropertyName(m.getName(), d); | |
} | |
return false; | |
} | |
private boolean isTypeDiscriminatorOuterProperty(AnnotatedMember m) { | |
var declaringClassBeanDef = config.introspect(typeFactory.constructType(m.getDeclaringClass())); | |
return declaringClassBeanDef.findProperties().stream() | |
.anyMatch(prop -> { | |
var annotation = prop.getAccessor().getAnnotation(JsonTypeInfo.class); | |
return annotation != null && isOuterProperty(annotation) && isTypeDiscriminatorPropertyName(m.getName(), annotation); | |
}); | |
} | |
private boolean isTypeDiscriminatorPropertyName(String propNameToCheck, JsonTypeInfo annotation) { | |
String expectedDiscriminatorPropName = getExpectedDiscriminatorPropName(annotation); | |
return propNameToCheck.equals(expectedDiscriminatorPropName) || propNameToCheck.equals(getterStyleNaming(expectedDiscriminatorPropName)); | |
} | |
private static String getExpectedDiscriminatorPropName(JsonTypeInfo annotation) { | |
String property = annotation.property(); | |
if (property.isEmpty()) { | |
return annotation.use().getDefaultPropertyName(); | |
} | |
return property; | |
} | |
private boolean isInnerProperty(JsonTypeInfo annotation) { | |
return annotation.include() == JsonTypeInfo.As.PROPERTY || annotation.include() == JsonTypeInfo.As.EXISTING_PROPERTY; | |
} | |
private boolean isOuterProperty(JsonTypeInfo annotation) { | |
return annotation.include() == JsonTypeInfo.As.EXTERNAL_PROPERTY; | |
} | |
private String getterStyleNaming(String propertyName) { | |
return "get" + Character.toUpperCase(propertyName.charAt(0)) + propertyName.substring(1); | |
} | |
@Override | |
public Version version() { | |
return Version.unknownVersion(); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment