CarstenWickner Github contribution chart
CarstenWickner Github Stats
CarstenWickner Most Used Languages

Activity

30 Sep 2022

CarstenWickner

Conditional required properties (if/then/else)

Hey,

Loosley related to #288, most likely just a question and not a request :) I again have this code:

 @JsonAlias("uid")
    @JsonPropertyDescription("The ID of the parameter. This ID must be unique in a given application.")
    public String id;

    // Compat with 4.x
    @Deprecated(forRemoval = true)
    @JsonProperty("uid")
    @JsonPropertyDescription("DEPRECATED: Use 'id' instead.")
    public String getUid() {
        return id;
    }; 

Now I would like the schema to require either of them to be set. AFAIK this is achieved using if/then/else in the schema (?) - how would I best go about configuring the generator appropriately? Thanks!

(Note: Both id and uid end up in the schema fine already, thanks to #288 one of them is even marked deprecated)

Forked On 30 Sep 2022 at 06:35:00

CarstenWickner

Hi @mduft,

I've created a complete example with a somewhat more generic approach (i.e., no need to hardcode any field names or relevant types) – but still very specific to your setup (deprecated method + field with alias replacing it). The schema also includes this part to ensure, a particular property is not allowed to be present:

{
  "properties": {
    "uid": false
  }
} 
public class DeprecatedAliasExample {

    public static void main(String[] args) {
        SchemaGeneratorConfigBuilder configBuilder = new SchemaGeneratorConfigBuilder(SchemaVersion.DRAFT_2020_12, OptionPreset.PLAIN_JSON)
                .with(Option.NONSTATIC_NONVOID_NONGETTER_METHODS, Option.FIELDS_DERIVED_FROM_ARGUMENTFREE_METHODS)
                .with(new JacksonModule(JacksonOption.INCLUDE_ONLY_JSONPROPERTY_ANNOTATED_METHODS));
        configBuilder.forTypesInGeneral().withTypeAttributeOverride(new DeprecatedFieldWithAliasHandler());
        SchemaGeneratorConfig config = configBuilder.build();
        SchemaGenerator generator = new SchemaGenerator(config);
        JsonNode jsonSchema = generator.generateSchema(TestType.class);

        System.out.println(jsonSchema.toPrettyString());
    }

    private static class DeprecatedFieldWithAliasHandler implements TypeAttributeOverrideV2 {

        @Override
        public void overrideTypeAttributes(ObjectNode node, TypeScope scope, SchemaGenerationContext context) {
            // check for deprecated methods
            ResolvedTypeWithMembers typeWithMembers = context.getTypeContext().resolveWithMembers(scope.getType());
            List<ResolvedMethod> deprecatedMethods = Stream.of(typeWithMembers.getMemberMethods())
                    .filter(method -> method.getArgumentCount() == 0
                            && method.getRawMember().getAnnotation(Deprecated.class) != null
                            && method.getRawMember().getAnnotation(JsonProperty.class) != null)
                    .collect(Collectors.toList());
            if (deprecatedMethods.isEmpty()) {
                // no relevant deprecated methods found
                return;
            }
            // determine whether there is any field replacing a deprecated method
            ResolvedField[] memberFields = typeWithMembers.getMemberFields();
            Map<String, ResolvedField> deprecatedFieldsAndReplacement = new HashMap<>();
            for (ResolvedMethod deprecatedMethod : deprecatedMethods) {
                String fieldName = deprecatedMethod.getRawMember().getAnnotation(JsonProperty.class).value();
                Stream.of(memberFields)
                        .filter(field -> Optional.ofNullable(field.getRawMember().getAnnotation(JsonAlias.class))
                        .map(JsonAlias::value).filter(aliasProperties -> Stream.of(aliasProperties).anyMatch(fieldName::equals)).isPresent())
                        .findFirst()
                        .ifPresent(replacingField -> deprecatedFieldsAndReplacement.put(fieldName, replacingField));
            }
            if (deprecatedFieldsAndReplacement.isEmpty()) {
                // no replacement found for the deprecated method(s)
                return;
            }
            ArrayNode allOfWrapper = node.withArray(context.getKeyword(SchemaKeyword.TAG_ALLOF));
            for (Map.Entry<String, ResolvedField> entry : deprecatedFieldsAndReplacement.entrySet()) {
                String oldFieldName = entry.getKey();
                String newFieldName = Optional.ofNullable(entry.getValue().getRawMember().getAnnotation(JsonProperty.class))
                        .map(JsonProperty::value)
                        .orElseGet(entry.getValue()::getName);
                ArrayNode oneOfWrapper = allOfWrapper.addObject().withArray(context.getKeyword(SchemaKeyword.TAG_ONEOF));
                this.requireOneButNotTheOther(newFieldName, oldFieldName, oneOfWrapper.addObject(), context);
                this.requireOneButNotTheOther(oldFieldName, newFieldName, oneOfWrapper.addObject(), context);
            }
        }

        private void requireOneButNotTheOther(String requiredField, String excludedField, ObjectNode node, SchemaGenerationContext context) {
            node.with(context.getKeyword(SchemaKeyword.TAG_PROPERTIES))
                    .put(excludedField, false);
            node.withArray(context.getKeyword(SchemaKeyword.TAG_REQUIRED))
                    .add(requiredField);
        }
    }

    public static class TestType {

        @JsonAlias("uid")
        @JsonPropertyDescription("The ID of the parameter. This ID must be unique in a given application.")
        public String id;

        @Deprecated
        @JsonProperty("uid")
        @JsonPropertyDescription("DEPRECATED: Use 'id' instead.")
        public String getUid() {
            return id;
        }
    }
} 

The produced JSON schema looks like this:

{
  "$schema" : "https://json-schema.org/draft/2020-12/schema",
  "type" : "object",
  "properties" : {
    "id" : {
      "type" : "string",
      "description" : "The ID of the parameter. This ID must be unique in a given application."
    },
    "uid" : {
      "type" : "string",
      "description" : "DEPRECATED: Use 'id' instead."
    }
  },
  "oneOf" : [
    {
      "properties" : { "uid" : false },
      "required" : [ "id" ]
    },
    {
      "properties" : { "id" : false },
      "required" : [ "uid" ]
    }
  ]
} 

Commented On 30 Sep 2022 at 06:35:00

CarstenWickner

chore: Prepare for next development iteration

Pushed On 29 Sep 2022 at 08:23:55

CarstenWickner

Releasing unreleased features

Is there any release plan/data for the now unreleased features? As I need to use them in production code, I would prefer not to depend on the current snapshot build.

Forked On 29 Sep 2022 at 08:21:35

CarstenWickner

As you wish: release v4.27.0 has been published.

To answer your question more in general: I usually just collect changes until they seem "big enough" to justify a release or until someone asks for it. 😉

Commented On 29 Sep 2022 at 08:21:35
Create Branch

CarstenWickner

Java JSON Schema Generator – creating JSON Schema (Draft 6, Draft 7, Draft 2019-09, or Draft 2020-12) from Java classes

On 29 Sep 2022 at 08:18:18

CarstenWickner

chore: release 4.27.0

Pushed On 29 Sep 2022 at 08:15:57

CarstenWickner

Redundant definition splitting and unwanted inlining in self-referencing type hierarchies

Given

Types

@JsonTypeInfo(use = JsonTypeInfo.Id.NAME)
@JsonSubTypes({
    @JsonSubTypes.Type(value = OrCondition.class, name = "or"),
    @JsonSubTypes.Type(value = EqualsCondition.class, name = "equals")
})
public sealed interface SearchCondition permits OrCondition, EqualsCondition {}

public record OrCondition(List<SearchCondition> conditions) implements SearchCondition {}

public record EqualsCondition(Object value) implements SearchCondition {} 

Config

var config = new SchemaGeneratorConfigBuilder(SchemaVersion.DRAFT_2020_12, OptionPreset.PLAIN_JSON)
    .with(new JacksonModule())
    .build();
var jsonSchema = new SchemaGenerator(config).generateSchema(SearchCondition.class);

System.out.println(jsonSchema.toPrettyString()); 

Problems

  • definitions are split in two parts (though technically correct, it's pretty confusing to read);
  • base class definition is inlined (again, it's correct, but not optimal);
  • there's no way to avoid definition splitting with current api (SchemaGenerationContext#createStandardDefinitionReference is the culprit, but the only available alternative - SchemaGenerationContext#createStandardDefinition - understandably leads to infinite recursion).

Using a custom definition provider, I was able to avoid base class definition inlining, but not the definition splitting.

Possible solutions

  • simple: make SchemaGenerationContextImpl#traverseGenericType (or it's overload) public - this will allow to cache definition ObjectNode without falling into infinite recursion;
  • proper: incorporate definition reference caching into library itself by adding something like SchemaGenerationContext#getOrCreateStandardDefinitionReference.

Schemas

Generated

{
    "$schema": "https://json-schema.org/draft/2020-12/schema",
    "$defs": {
        "EqualsCondition-1": {
            "type": "object",
            "properties": {
                "value": {}
            }
        },
        "EqualsCondition-2": {
            "$ref": "#/$defs/EqualsCondition-1",
            "type": "object",
            "properties": {
                "@type": {
                    "const": "equals"
                }
            },
            "required": [
                "@type"
            ]
        },
        "OrCondition-1": {
            "type": "object",
            "properties": {
                "conditions": {
                    "type": "array",
                    "items": {
                        "anyOf": [
                            {
                                "$ref": "#/$defs/OrCondition-2"
                            },
                            {
                                "$ref": "#/$defs/EqualsCondition-2"
                            }
                        ]
                    }
                }
            }
        },
        "OrCondition-2": {
            "$ref": "#/$defs/OrCondition-1",
            "type": "object",
            "properties": {
                "@type": {
                    "const": "or"
                }
            },
            "required": [
                "@type"
            ]
        }
    },
    "anyOf": [
        {
            "$ref": "#/$defs/OrCondition-2"
        },
        {
            "$ref": "#/$defs/EqualsCondition-2"
        }
    ]
} 

Ideal

{
    "$schema": "https://json-schema.org/draft/2020-12/schema",
    "$defs": {
        "SearchCondition": {
            "oneOf": [
                {
                    "$ref": "#/$defs/EqualsCondition"
                },
                {
                    "$ref": "#/$defs/OrCondition"
                }
            ]
        },
        "EqualsCondition": {
            "type": "object",
            "properties": {
                "@type": {
                    "const": "equals"
                },
                "value": {}
            },
            "required": [
                "@type",
                "value"
            ]
        },
        "OrCondition": {
            "type": "object",
            "properties": {
                "@type": {
                    "const": "or"
                },
                "conditions": {
                    "type": "array",
                    "items": {
                        "$ref": "#/$defs/SearchCondition"
                    }
                }
            },
            "required": [
                "@type",
                "conditions"
            ]
        }
    },
    "$ref": "#/$defs/SearchCondition"
} 

Forked On 29 Sep 2022 at 07:57:10

CarstenWickner

HI @meztihn,

I've been working on this a bit. And this test class illustrates the current/improved behavior, which produces a schema with explicit definitions both for a supertype and its subtypes:

{
    "$schema": "https://json-schema.org/draft/2019-09/schema",
    "$defs": {
        "TestSubClassA": {
            "allOf": [{
                    "type": "object",
                    "properties": {
                        "aProperty": {
                            "type": "string"
                        }
                    }
                }, {
                    "type": "object",
                    "properties": {
                        "@type": {
                            "const": "SubClassA"
                        }
                    },
                    "required": ["@type"]
                }]
        },
        "TestSubClassB": {
            "allOf": [{
                    "type": "object",
                    "properties": {
                        "bProperty": {
                            "type": "integer"
                        }
                    }
                }, {
                    "type": "object",
                    "properties": {
                        "@type": {
                            "const": "SubClassB"
                        }
                    },
                    "required": ["@type"]
                }]
        },
        "TestSuperClass": {
            "anyOf": [{
                    "$ref": "#/$defs/TestSubClassA"
                }, {
                    "$ref": "#/$defs/TestSubClassB"
                }]
        }
    },
    "type": "object",
    "properties": {
        "supertypeA": {
            "$ref": "#/$defs/TestSuperClass",
            "description": "A member description"
        },
        "supertypeB": {
            "$ref": "#/$defs/TestSuperClass"
        }
    }
} 

This is being achieved via this configuration:

SchemaGeneratorConfig config = new SchemaGeneratorConfigBuilder(SchemaVersion.DRAFT_2019_09, OptionPreset.PLAIN_JSON)
        .with(Option.DEFINITIONS_FOR_MEMBER_SUPERTYPES)
        .with(new JacksonModule(JacksonOption.ALWAYS_REF_SUBTYPES))
        .build(); 
  • The Option.DEFINITIONS_FOR_MEMBER_SUPERTYPES is new and needs to be used with care, as it reduces the information available to the subtype schemas (which is the reason, that was previously not possible). In fact, enabling this may result in an invalid schema being generated, e.g., if per-property overrides of the jackson subtype handling is present. While jackson explicitly supports this (as well as this schema generation library) it may not be very common. In projects, where there is only a single subtype handling defined on a given supertype, which is globally applied, this Option should be acceptable to use.
  • The JacksonOption.ALWAYS_REF_SUBTYPES is also new and makes use of yet another new standard feature, that each CustomDefinition can now explicitly state whether it should end up in the $defs block. Previously, it was only possible to say: always INLINE or apply the STANDARD behavior (i.e., move into $defs if referenced at least twice). Now the custom definition can also indicate to ALWAYS_REF, i.e., always be referenced even just from a single place.

I did also look into consolidating the subtype schema's allOf array, but that is quite involved and I don't have the time/capacity to see this through at the moment. I hope this is good enough for now.

Commented On 29 Sep 2022 at 07:57:10

CarstenWickner

feat: Enable Custom Definition never to be inlined (#291)

  • fix: avoid allOf with single item

  • feat: Introducing CustomDefinition.DefinitionType.ALWAYS_REF

  • feat: Introducing JacksonOption.ALWAYS_REF_SUBTYPES

Pushed On 29 Sep 2022 at 07:45:16

CarstenWickner

feat: Enable Custom Definition never to be inlined

Created On 29 Sep 2022 at 07:45:16

CarstenWickner

feat: Enable Custom Definition never to be inlined

Fixes #280.

In main generator: introducing the option to never inline a custom definition even when only referenced once without having to globally enable Option.DEFINITIONS_FOR_ALL_OBJECTS.

In jackson module: introducing JacksonOption.ALWAYS_REF_SUBTYPES in order to apply the new behavior to subtype (wrapper) definitions.

Forked On 29 Sep 2022 at 07:42:18

CarstenWickner

Not a good enough reason to introduce a dependency just for this kind of annotation. Maybe later.
On 29 Sep 2022 at 07:42:18

CarstenWickner

feat: Enable Custom Definition never to be inlined

Fixes #280.

In main generator: introducing the option to never inline a custom definition even when only referenced once without having to globally enable Option.DEFINITIONS_FOR_ALL_OBJECTS.

In jackson module: introducing JacksonOption.ALWAYS_REF_SUBTYPES in order to apply the new behavior to subtype (wrapper) definitions.

Merged On 29 Sep 2022 at 07:42:18

CarstenWickner

Commented On 29 Sep 2022 at 07:42:18

CarstenWickner

feat: Enable type definition for members declaring a supertype (#285)

  • feat: consolidate nullable subtype schema

  • feat: Option for member supertype definition

  • fix: slightly improve schema clean-up logic

Pushed On 29 Sep 2022 at 07:30:22

CarstenWickner

Merge branch 'main' into allow-super-definition

Pushed On 29 Sep 2022 at 07:30:22

CarstenWickner

feat: Enable Custom Definition never to be inlined

Created On 29 Sep 2022 at 07:28:29
Create Branch
CarstenWickner In victools/jsonschema-generator Create Branchallow-super-definition

CarstenWickner

Java JSON Schema Generator – creating JSON Schema (Draft 6, Draft 7, Draft 2019-09, or Draft 2020-12) from Java classes

On 29 Sep 2022 at 07:23:42

CarstenWickner

Conditional required properties (if/then/else)

Hey,

Loosley related to #288, most likely just a question and not a request :) I again have this code:

 @JsonAlias("uid")
    @JsonPropertyDescription("The ID of the parameter. This ID must be unique in a given application.")
    public String id;

    // Compat with 4.x
    @Deprecated(forRemoval = true)
    @JsonProperty("uid")
    @JsonPropertyDescription("DEPRECATED: Use 'id' instead.")
    public String getUid() {
        return id;
    }; 

Now I would like the schema to require either of them to be set. AFAIK this is achieved using if/then/else in the schema (?) - how would I best go about configuring the generator appropriately? Thanks!

(Note: Both id and uid end up in the schema fine already, thanks to #288 one of them is even marked deprecated)

Forked On 29 Sep 2022 at 08:33:39

CarstenWickner

I'd suggest using a Type Attribute Override and then manually adding this part to the schema:

{
  "oneOf": [{
      "required": ["id"]
    },{
      "required": ["uid"]
  }]
} 

Don't have the capacity right now, to provide the necessary configuration.

Commented On 29 Sep 2022 at 08:33:39

CarstenWickner

Schema Generation fails with Duplicate Key exception when using JacksonModule and Option.DEFINITIONS_FOR_ALL_OBJECTS

I have a polymorphic structure that looks like:

@JsonTypeInfo(
      use = JsonTypeInfo.Id.CLASS,
      property = "class",
      defaultImpl = Dog.class
)
public interface Pet {}

public class Dog implements Pet {} 

and i have a generator config

JacksonModule jacksonModule = new JacksonModule(
                JacksonOption.RESPECT_JSONPROPERTY_ORDER,
                JacksonOption.RESPECT_JSONPROPERTY_REQUIRED,
                JacksonOption.FLATTENED_ENUMS_FROM_JSONPROPERTY
 );
SchemaGeneratorConfigBuilder configBuilder = new SchemaGeneratorConfigBuilder(MAPPER,
                SchemaVersion.DRAFT_2020_12, new OptionPreset(
                Option.SCHEMA_VERSION_INDICATOR,
                Option.ADDITIONAL_FIXED_TYPES,
                Option.FLATTENED_ENUMS,
                Option.FLATTENED_OPTIONALS,
                Option.VALUES_FROM_CONSTANT_FIELDS,
                Option.PUBLIC_NONSTATIC_FIELDS,
                Option.NONPUBLIC_NONSTATIC_FIELDS_WITH_GETTERS,
                Option.ALLOF_CLEANUP_AT_THE_END,
                Option.DEFINITIONS_FOR_ALL_OBJECTS
        )).with(jacksonModule); 

which is almost equivalent to OptionPreset.PLAIN_JSON except i added DEFINITIONS_FOR_ALL_OBJECTS.

when i try to generate a schema for this class. I get an exception: SchemaDefinitionNamingStrategy of type CleanSchemaDefinitionNamingStrategy produced duplicate keys because it appears to have tried to create 2 definitions for Pet, one through DEFINITIONS_FOR_ALL_OBJECTS and one through Jackson polymorphic types lookup. I would like to be able to generate definitions for all my objects, this includes those found by Jackson. Is this possible?

Forked On 28 Sep 2022 at 09:28:46

CarstenWickner

I'm working on improving the subtype handling somewhat. However I now believe the most likely cause for the duplicate key error is the fact that you have two classes with the same simple class name in your model hierarchy (in different packages). You may have to configure another naming strategy then, that considers the package name as well.

Commented On 28 Sep 2022 at 09:28:46

CarstenWickner

Add support for annotated type parameters

Given a class

 public class Dummy {
  public Map<String, @Max(10) Integer> map;
} 

which is annotated using jakarta bean validation annotations. The schema generated currently using the jakarta module is

{
  "type":"object",
  "properties":{
    "map":{
      "type":"object",
      "additionalProperties":{
        "type":"integer"
      }
    }
  }
} 

But the following schema would better conform to the desired intention:

{
  "type":"object",
  "properties":{
    "map":{
      "type":"object",
      "additionalProperties":{
        "type":"integer",
        "maximum":10
      }
    }
  }
} 

A similar behavior would also be desirable for other collections (e.g. List<@Max(10) Integer).

Forked On 28 Sep 2022 at 09:25:09

CarstenWickner

I agree that supporting this would be beneficial. Right now, I don't know how to access these annotations properly. I'm open to suggestions (and pull requests). 😉

Commented On 28 Sep 2022 at 09:25:09

CarstenWickner

Support for 'deprecated'

It would be nice to have support for the 'deprecated' meta-data annotation (https://json-schema.org/draft/2020-12/json-schema-validation.html#section-9.3)

We have DTOs like so in Java:

 @JsonAlias("uid")
    @JsonPropertyDescription("The ID of the parameter. This ID must be unique in a given application.")
    public String id;

    // Compat with 4.x
    @Deprecated(forRemoval = true)
    @JsonProperty("uid")
    @JsonPropertyDescription("DEPRECATED: Use 'id' instead.")
    public String getUid() {
        return id;
    }; 

The whole construct to allow the usage of 'uid' in existing data, whilst using the new 'id' whenever writing (and internally when using).

Forked On 28 Sep 2022 at 09:21:24

CarstenWickner

Hi @mduft,

You can achieve this through configuration already. E.g., via Instance Attribute Overrides.

configBuilder.forMethods().withInstanceAttributeOverride((node, method, context) -> {
    if (method.getAnnotation(Deprecated.class) != null) {
        node.put("deprecated", true);
    }
}); 

Im assuming you are including Option.NONSTATIC_NONVOID_NONGETTER_METHODSas well asOption.FIELDS_DERIVED_FROM_ARGUMENTFREE_METHODS` and explicitly ignore methods that have parameters then.

Commented On 28 Sep 2022 at 09:21:24

CarstenWickner

feat: Enable type definition for members declaring a supertype

Created On 25 Sep 2022 at 09:54:14

CarstenWickner

feat: Enable type definition for members declaring a supertype (#285)

  • feat: consolidate nullable subtype schema

  • feat: Option for member supertype definition

  • fix: slightly improve schema clean-up logic

Pushed On 25 Sep 2022 at 09:54:15

CarstenWickner

fix: avoid allOf with single item

Pushed On 25 Sep 2022 at 09:41:42

CarstenWickner

chore(docs): add line-break in adjusted JavaDocs

Pushed On 25 Sep 2022 at 09:08:14

CarstenWickner

feat: Enable type definition for members declaring a supertype

Created On 25 Sep 2022 at 08:22:40
Create Branch
CarstenWickner In victools/jsonschema-generator Create Branchenhanced-allof-merge

CarstenWickner

Java JSON Schema Generator – creating JSON Schema (Draft 6, Draft 7, Draft 2019-09, or Draft 2020-12) from Java classes

On 25 Sep 2022 at 08:14:54

CarstenWickner

chore(docs): improve JavaDoc

Pushed On 25 Sep 2022 at 08:13:43
Create Branch
CarstenWickner In victools/jsonschema-generator Create Branchallow-super-definition

CarstenWickner

Java JSON Schema Generator – creating JSON Schema (Draft 6, Draft 7, Draft 2019-09, or Draft 2020-12) from Java classes

On 25 Sep 2022 at 08:12:24