'Custom property keywords in JSON Schema using Jackson annotations

I would like to create custom property keywords that would be preset in the JSON schema generated using Jackson. This would be something similar to what JsonPropertyDescription annotation does, but for custom annotations and keywords. For example, I'd like to be able to annotate a property with a custom JsonPropertyLabel annotation that would add a "label" keyword in the property description.

I did manage to do a similar thing using a custom JsonSchema and JsonSchemaFactory like this:

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.BeanProperty;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.module.jsonSchema.JsonSchema;
import com.fasterxml.jackson.module.jsonSchema.customProperties.ValidationSchemaFactoryWrapper;
import com.fasterxml.jackson.module.jsonSchema.factories.FormatVisitorFactory;
import com.fasterxml.jackson.module.jsonSchema.factories.JsonSchemaFactory;
import com.fasterxml.jackson.module.jsonSchema.factories.SchemaFactoryWrapper;
import com.fasterxml.jackson.module.jsonSchema.factories.VisitorContext;
import com.fasterxml.jackson.module.jsonSchema.factories.WrapperFactory;
import com.fasterxml.jackson.module.jsonSchema.types.ObjectSchema;
import com.fasterxml.jackson.annotation.JacksonAnnotation;
import com.fasterxml.jackson.module.jsonSchema.validation.AnnotationConstraintResolver;


public class SchemaUtils {

    @Target(ElementType.FIELD)
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @JacksonAnnotation
    public @interface JsonPropertyLabel {
        String value();
    }

    public static String getPrettyClassSchema(Class type) throws JsonProcessingException {
        ObjectMapper objectMapper = new ObjectMapper();
        return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(ConfigurationUtils.getJsonSchema(type, objectMapper));
    }

    static JsonSchema getJsonSchema(Class type, ObjectMapper objectMapper) throws JsonMappingException {
        SchemaFactoryWrapper schemaFactoryWrapper = new CustomJsonSchemaFactoryWrapper();
        objectMapper.acceptJsonFormatVisitor(objectMapper.constructType(type), schemaFactoryWrapper); 
        return schemaFactoryWrapper.finalSchema();
    }

    static class CustomJsonSchemaFactoryWrapper extends ValidationSchemaFactoryWrapper {
    private static class SchemaFactoryWrapperFactory extends WrapperFactory {
      private SchemaFactoryWrapperFactory() {
      }

      public SchemaFactoryWrapper getWrapper(SerializerProvider p) {
        return this.getWrapper(p, null);
      }

      public SchemaFactoryWrapper getWrapper(SerializerProvider p, VisitorContext rvc) {
        SchemaFactoryWrapper wrapper = new CustomJsonSchemaFactoryWrapper();
        wrapper.setProvider(p);
        if(rvc != null) {
          wrapper.setVisitorContext(rvc);
        }
        return wrapper;
      }
    }

    public CustomJsonSchemaFactoryWrapper() {
      super(new AnnotationConstraintResolver());
      schemaProvider = new CustomJsonSchemaFactory();
      visitorFactory = new FormatVisitorFactory(new SchemaFactoryWrapperFactory());
    }

    static class CustomJsonSchemaFactory extends JsonSchemaFactory {
      @Override
      public ObjectSchema objectSchema() {
        return new CustomJsonSchema();
      }

      static class CustomJsonSchema extends ObjectSchema {
        @JsonProperty private String label;

        @Override
        public void enrichWithBeanProperty(BeanProperty beanProperty) {
          super.enrichWithBeanProperty(beanProperty);
          JsonPropertyLabel labelAnnotation = beanProperty.getAnnotation(JsonPropertyLabel.class);
          if (labelAnnotation != null) {
            this.label = labelAnnotation.value();
          }
        }
      }
    }
  }
}

However, when trying this code on the following class:

public class POJOExample {
  @JsonPropertyDescription("first property description")
  @JsonPropertyLabel("first  property label")
  public PropertyExample first;

  @JsonPropertyDescription("second property description")
  @JsonPropertyLabel("second property label")
  public PropertyExample second;

  public static class PropertyExample {
    public String value;
  }
}

The JSON schema for this comes out as:

{
  "type" : "object",
  "id" : "urn:jsonschema:test:POJOExample",
  "properties" : {
    "first" : {
      "type" : "object",
      "id" : "urn:jsonschema:test:POJOExample:PropertyExample",
      "description" : "first property description",
      "properties" : {
        "value" : {
          "type" : "string"
        }
      },
      "label" : "first  property label"
    },
    "second" : {
      "type" : "object",
      "$ref" : "urn:jsonschema:test:POJOExample:PropertyExample",
      "description" : "second property description"
    }
  }
}

As you can see, "first" has both "description" and "label", bud "second" has only "description". I suppose this implementation bundles the "label" as a type feature available through reference. How would I make the "label" behave as "description" and have it on the "second" property as well?

Furthermore, it would be optimal if I were able to do this dynamically for use-cases where I don't know the keyword string value in advance but at run time. For example, having an annotation JsonPropertyKeyword like so:

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@JacksonAnnotation
public @interface JsonPropertyKeyword {
  String keyword();
  String value();
}

This way I could annotate properties with @JsonPropertyKeyword(keyword="description", value="some description") and achieve the same effect as with @JsonPropertyDescription("some description"). Is this possible?



Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source