Skip to content

Instantly share code, notes, and snippets.

@Chuckame
Created January 22, 2024 16:12
Show Gist options
  • Save Chuckame/0e2b37e3cd09e1e525bacfbd4f9d028e to your computer and use it in GitHub Desktop.
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
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();
}
}
}
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);
}
}
}
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