Skip to content

Commit

Permalink
feat: json schema custom generator for serializable fields + proxy bu…
Browse files Browse the repository at this point in the history
…ilding for specs before generation
  • Loading branch information
matteo-s committed Sep 5, 2024
1 parent a8ab8cb commit c5e1664
Show file tree
Hide file tree
Showing 3 changed files with 143 additions and 2 deletions.
6 changes: 5 additions & 1 deletion modules/commons/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,11 @@
<artifactId>jsonschema-module-swagger-2</artifactId>
<version>${jsonschema.version}</version>
</dependency>

<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy</artifactId>
<version>1.14.17</version>
</dependency>

<!-- JUnit 5 Jupiter API for writing tests -->
<dependency>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package it.smartcommunitylabdhub.commons.jackson.mixins;

import io.swagger.v3.oas.annotations.media.Schema;

@Schema(
anyOf = {
SerializableMixin.StringField.class,
SerializableMixin.NumberField.class,
// SerializableMixin.IntegerField.class,
SerializableMixin.BooleanField.class,
SerializableMixin.SerializableField.class,
SerializableMixin.SerializableField[].class,
},
nullable = true,
title = "object"
)
public class SerializableMixin {

@Schema(implementation = String.class, title = "string", defaultValue = "")
public class StringField {}

@Schema(implementation = Number.class, title = "number", defaultValue = "")
public class NumberField {}

@Schema(implementation = Integer.class, title = "integer", defaultValue = "")
public class IntegerField {}

@Schema(implementation = Boolean.class, title = "boolean", defaultValue = "")
public class BooleanField {}

@Schema(
anyOf = {
SerializableMixin.StringField.class,
SerializableMixin.NumberField.class,
// SerializableMixin.IntegerField.class,
SerializableMixin.BooleanField.class,
SerializableMixin.SerializableField.class,
SerializableMixin.SerializableField[].class,
},
// nullable = true,
title = "object"
)
public class SerializableField {}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package it.smartcommunitylabdhub.commons.utils;

import aj.org.objectweb.asm.Type;
import com.fasterxml.jackson.annotation.JsonUnwrapped;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.victools.jsonschema.generator.ConfigFunction;
import com.github.victools.jsonschema.generator.CustomDefinition;
import com.github.victools.jsonschema.generator.FieldScope;
import com.github.victools.jsonschema.generator.Option;
import com.github.victools.jsonschema.generator.OptionPreset;
Expand All @@ -15,16 +18,35 @@
import com.github.victools.jsonschema.module.jakarta.validation.JakartaValidationModule;
import com.github.victools.jsonschema.module.jakarta.validation.JakartaValidationOption;
import com.github.victools.jsonschema.module.swagger2.Swagger2Module;
import io.swagger.v3.oas.annotations.media.Schema;
import it.smartcommunitylabdhub.commons.annotations.common.SpecType;
import it.smartcommunitylabdhub.commons.jackson.annotations.JsonSchemaIgnore;
import it.smartcommunitylabdhub.commons.jackson.introspect.JsonSchemaAnnotationIntrospector;
import it.smartcommunitylabdhub.commons.jackson.mixins.SerializableMixin;
import it.smartcommunitylabdhub.commons.models.specs.Spec;
import java.io.Serializable;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import net.bytebuddy.ByteBuddy;
import net.bytebuddy.asm.AsmVisitorWrapper;
import net.bytebuddy.jar.asm.AnnotationVisitor;
import net.bytebuddy.jar.asm.FieldVisitor;
import net.bytebuddy.matcher.ElementMatchers;
import net.bytebuddy.utility.OpenedClassReader;
import org.springframework.util.StringUtils;

//TODO refactor into a factory
public final class SchemaUtils {

public static final String FIELDS_PREFIX = "fields.";
public static final String SPECS_PREFIX = "specs.";
private static final List<Class<?>> SERIALIZABLE_TYPES = Arrays.asList(
String.class,
Number.class,
Boolean.class,
Integer.class
);

public static final SchemaGenerator GENERATOR;

Expand Down Expand Up @@ -95,12 +117,55 @@ public final class SchemaUtils {
}

return null;
})
.withIgnoreCheck(field -> {
return field.getAnnotation(JsonSchemaIgnore.class) != null;
});

configBuilder
.forTypesInGeneral()
.withTitleResolver(specTypeResolver("title"))
.withDescriptionResolver(specTypeResolver("description"));
.withDescriptionResolver(specTypeResolver("description"))
.withCustomDefinitionProvider((javaType, context) -> {
//redefine Serializable via mixin with annotations, and inline
return Serializable.class.equals(javaType.getErasedType())
? new CustomDefinition(
context.createDefinition(context.getTypeContext().resolve(SerializableMixin.class)),
CustomDefinition.DefinitionType.INLINE,
CustomDefinition.AttributeInclusion.YES
)
: null;
})
.withCustomDefinitionProvider((javaType, context) -> {
Schema sa = javaType.getErasedType().getAnnotation(Schema.class);
if (sa != null && sa.implementation() != Void.class) {
//override with implementation type, inline
return new CustomDefinition(
context.createDefinition(context.getTypeContext().resolve(sa.implementation())),
CustomDefinition.DefinitionType.INLINE,
CustomDefinition.AttributeInclusion.YES
);
}

return null;
})
.withTypeAttributeOverride((node, scope, context) -> {
//for custom defined overrides also inject props from schema, since those are skipper by other modules
if (SerializableMixin.class.getPackage().equals(scope.getType().getErasedType().getPackage())) {
Schema sa = scope.getType().getErasedType().getAnnotation(Schema.class);
if (sa != null) {
if (StringUtils.hasText(sa.title())) {
node.put("title", sa.title());
}
if (StringUtils.hasText(sa.description())) {
node.put("description", sa.description());
}
if (StringUtils.hasText(sa.defaultValue())) {
node.put("defaultValue", sa.defaultValue());
}
}
}
});

GENERATOR = new SchemaGenerator(configBuilder.build());
}
Expand All @@ -109,6 +174,34 @@ public static JsonNode schema(Class<? extends Spec> clazz) {
return GENERATOR.generateSchema(clazz);
}

public static <T extends Spec> Class<? extends T> proxy(Class<T> clazz) {
return new ByteBuddy()
.redefine(clazz)
.visit(
new AsmVisitorWrapper.ForDeclaredFields()
.field(
//redefine fields marked with ignore
ElementMatchers.isAnnotatedWith(JsonSchemaIgnore.class),
(instrumentedType, fieldDescription, fieldVisitor) ->
new FieldVisitor(OpenedClassReader.ASM_API, fieldVisitor) {
@Override
public AnnotationVisitor visitAnnotation(String description, boolean visible) {
//remove jsonUnwrapped to resolve issue with unwrapped fields skipping ignore
if (Type.getDescriptor(JsonUnwrapped.class).equals(description)) {
return null;
}

return super.visitAnnotation(description, visible);
}
}
)
)
.name(clazz.getName() + "Proxy")
.make()
.load(clazz.getClassLoader())
.getLoaded();
}

private static ConfigFunction<TypeScope, String> specTypeResolver(String value) {
return typeScope -> {
return Optional
Expand Down

0 comments on commit c5e1664

Please sign in to comment.