-
Notifications
You must be signed in to change notification settings - Fork 59
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Adding a new property to ConfigBuilder #319
Comments
Hi @theonepichael, The easiest way to do this would probably be to just manipulate the schema for each type (or just the ones you care about) via a "Type Attribute Override", e.g. like the one below: config.forTypesInGeneral().withTypeAttributeOverride((schema, scope, context) -> schema
.with(context.getKeyword(SchemaKeyword.TAG_PROPERTIES))
.set(context.getKeyword(SchemaKeyword.TAG_SCHEMA),
context.createStandardDefinitionReference(scope.getContext().resolve(String.class), null))); |
When calling
I appreciate the response. |
Hi @theonepichael, Which Jackson library version are you using?
Please provide a full stack trace, in order to get a better feeling where that error is being raised. |
I'm using version 2.14.2
|
A follow up: {
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Example",
"anyOf": [
{
"required": [
"foo"
]
},
{
"required": [
"bar"
]
},
{
"required": [
"baz"
]
},
{
"required": [
"fizz"
]
},
{
"required": [
"buzz"
]
}
],
"properties": {
"$schema": {
"type": "string"
},
"foo": {
"description": "foo",
"type": "number",
"minimum": 0.0,
"maximum": 360.0
},
"bar": {
"description": "bar",
"type": "number",
"minimum": 0.0
},
"baz": {
"description": "baz",
"type": "number"
},
"fizz": {
"description": "fizz",
"type": "number",
"minimum": 0.0
},
"buzz": {
"description": "buzz",
"type": "number",
"minimum": 0.0
}
},
"additionalProperties": false
} One line explanation of the schema: |
Hi @theonepichael, I can't reproduce your exact error, but to make it work I had to add an extra filter in the public class TypeAttributeOverride319 {
public static void main(String[] args) {
SchemaGeneratorConfigBuilder configBuilder = new SchemaGeneratorConfigBuilder(SchemaVersion.DRAFT_2020_12, OptionPreset.PLAIN_JSON);
configBuilder.forTypesInGeneral().withTypeAttributeOverride((schema, scope, context) -> {
if (scope.getType().getErasedType() == TestType.class) {
ObjectNode propertiesNode = (ObjectNode) schema
.putIfAbsent(context.getKeyword(SchemaKeyword.TAG_PROPERTIES), schema.objectNode());
propertiesNode.set(context.getKeyword(SchemaKeyword.TAG_SCHEMA),
context.createStandardDefinition(context.getTypeContext().resolve(String.class), null));
}
});
SchemaGeneratorConfig config = configBuilder.build();
SchemaGenerator generator = new SchemaGenerator(config);
JsonNode jsonSchema = generator.generateSchema(TestType.class);
System.out.println(jsonSchema.toPrettyString());
}
public static class TestType {
public String otherProperty;
}
} This produces: {
"$schema" : "https://json-schema.org/draft/2020-12/schema",
"type" : "object",
"properties" : {
"otherProperty" : {
"type" : "string"
},
"$schema" : {
"type" : "string"
}
}
} Regarding your follow-up question:
public class AnyOfRequired319 {
public static void main(String[] args) {
SchemaGeneratorConfigBuilder configBuilder = new SchemaGeneratorConfigBuilder(SchemaVersion.DRAFT_7, OptionPreset.PLAIN_JSON);
configBuilder.forTypesInGeneral()
// use CustomDefinitionProvider to add anyOf that marks each individual property as required
.withCustomDefinitionProvider(new AnyPropertyRequiredCustomDefinitionProvider(TestType.class))
// preserve declaration order of properties (instead of default alphabetic sorting)
.withPropertySorter((a, b) -> 0);
configBuilder
// include JakartaValidationModule in order to pick up minimum/maximum constraints
.with(new JakartaValidationModule())
// include JacksonModule in order to pick up annotated descriptions
.with(new JacksonModule());
SchemaGeneratorConfig config = configBuilder.build();
SchemaGenerator generator = new SchemaGenerator(config);
JsonNode jsonSchema = generator.generateSchema(TestType.class);
System.out.println(jsonSchema.toPrettyString());
}
private static class AnyPropertyRequiredCustomDefinitionProvider implements CustomDefinitionProviderV2 {
private final Class<?> erasedTargetType;
public AnyPropertyRequiredCustomDefinitionProvider(Class<?> erasedTargetType) {
this.erasedTargetType = erasedTargetType;
}
@Override
public CustomDefinition provideCustomSchemaDefinition(ResolvedType javaType, SchemaGenerationContext context) {
if (javaType.getErasedType() != this.erasedTargetType) {
return null;
}
ObjectNode schema = context.createStandardDefinition(javaType, this);
ObjectNode propertiesNode = (ObjectNode) schema.putIfAbsent(context.getKeyword(SchemaKeyword.TAG_PROPERTIES), schema.objectNode());
if (!propertiesNode.isEmpty()) {
// wrap additional anyOf into allOf in order to avoid conflicting with potentially existing anyOf
// if the allOf is unnecessary, it will be automatically removed in the end
ArrayNode anyOfNode = schema.withArray(context.getKeyword(SchemaKeyword.TAG_ALLOF))
.addObject().withArray(context.getKeyword(SchemaKeyword.TAG_ANYOF));
propertiesNode.fieldNames()
.forEachRemaining(fieldName -> anyOfNode.addObject()
.withArray(context.getKeyword(SchemaKeyword.TAG_REQUIRED))
.add(fieldName));
}
// also add the additional "$schema" property while we're at it
propertiesNode.set(context.getKeyword(SchemaKeyword.TAG_SCHEMA),
context.createStandardDefinition(context.getTypeContext().resolve(String.class), null));
return new CustomDefinition(schema, CustomDefinition.DefinitionType.STANDARD, CustomDefinition.AttributeInclusion.YES);
}
}
public static class TestType {
@DecimalMin("0.0")
@DecimalMax("360.0")
@JsonPropertyDescription("foo")
public double foo;
@DecimalMin("0.0")
@JsonPropertyDescription("bar")
public double bar;
@JsonPropertyDescription("baz")
public double baz;
@DecimalMin("0.0")
@JsonPropertyDescription("fizz")
public double fizz;
@DecimalMin("0.0")
@JsonPropertyDescription("buzz")
public double buzz;
}
} This produces: {
"$schema" : "http://json-schema.org/draft-07/schema#",
"type" : "object",
"properties" : {
"foo" : {
"type" : "number",
"description" : "foo",
"minimum" : 0.0,
"maximum" : 360.0
},
"bar" : {
"type" : "number",
"description" : "bar",
"minimum" : 0.0
},
"baz" : {
"type" : "number",
"description" : "baz"
},
"fizz" : {
"type" : "number",
"description" : "fizz",
"minimum" : 0.0
},
"buzz" : {
"type" : "number",
"description" : "buzz",
"minimum" : 0.0
},
"$schema" : {
"type" : "string"
}
},
"anyOf" : [ {
"required" : [ "foo" ]
}, {
"required" : [ "bar" ]
}, {
"required" : [ "baz" ]
}, {
"required" : [ "fizz" ]
}, {
"required" : [ "buzz" ]
} ]
} I hope that helps. 😃 |
Thanks for the response. Looks like the schema.with(context.getKeyword(SchemaKeyword.TAG_PROPERTIES)) The suggested alternative of the Including pom.xml: <parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.0.4</version>
<relativePath /> <!-- lookup parent from repository -->
</parent>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.24</version>
</dependency>
<dependency>
<groupId>com.github.victools</groupId>
<artifactId>jsonschema-generator</artifactId>
<version>4.28.0</version>
</dependency>
<dependency>
<groupId>com.github.victools</groupId>
<artifactId>jsonschema-module-jackson</artifactId>
<version>4.28.0</version>
</dependency>
<dependency>
<groupId>com.github.victools</groupId>
<artifactId>jsonschema-module-swagger-2</artifactId>
<version>4.21.0</version>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.0.2</version>
</dependency>
</dependencies> |
Hi @theonepichael, I've edited my previous answer to replace the usage of the deprecated line with this. ObjectNode propertiesNode = (ObjectNode) schema.putIfAbsent(context.getKeyword(SchemaKeyword.TAG_PROPERTIES), schema.objectNode()); If one assumes, that a (ObjectNode) schema.get(context.getKeyword(SchemaKeyword.TAG_PROPERTIES)) |
Thanks for clearing that up. I have an additional follow up. I want to produce a schema that looks like the following. {
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Example",
"required": [
"alpha",
"beta",
"sigma"
],
"properties": {
"$schema": {
"type": "string"
},
"alpha": {
"description": "alpha",
"type": "string"
},
"beta": {
"$ref": "./BetaSchema.json"
},
"sigma": {
"description": "sigma",
"type": "number"
},
"omega": {
"type": "array",
"items": {
"oneOf": [
{
"$ref": "./ThetaSchema.json"
},
{
"$ref": "./TauSchema.json"
}
]
}
}
},
"additionalProperties": false
} One line explanation of the schema: (WHERE I'M CURRENTLY AT) The current output is: {
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Example",
"required": [
"alpha",
"beta",
"sigma"
],
"properties": {
"$schema": {
"type": "string"
},
"alpha": {
"description": "alpha",
"type": "string"
},
"beta": {
"$ref": "./BetaSchema.json"
},
"sigma": {
"description": "sigma",
"type": "number"
},
"omega": {
"type": "array",
"items": {
"anyOf": [ // should be "oneOf"
{
//theta schema is inlined -> should be "$ref" : "..."
},
{
//tau schema is inlined -> should be "$ref" : "..."
}
]
}
}
},
"additionalProperties": false
} Note: The "Omega" class is the supertype of Theta and Tau. So only one of those subclass types is allowed in the array class Example {
/// required properties omitted
@Schema(name = "omega", type = "array", oneOf = {Theta.class, Tau.class})
private List<Omega> omega;
}
@Schema(
title = "Theta",
description = "Theta",
additionalProperties = Schema.AdditionalPropertiesValue.FALSE,
ref = "./ThetaSchema.json")
class Theta extends Omega {}
// Implementation omitted for brevity
@Schema(
title = "Tau",
description = "Tau",
additionalProperties = Schema.AdditionalPropertiesValue.FALSE,
ref = "./TauSchema.json")
class Tau extends Omega {} What changes do I need to make to get to my desired output? |
Hi @theonepichael, It's okay to create a new question btw., no need to keep adding more things to the same issue. 😜 That being said, your annotation of the @ArraySchema(schema = @Schema(oneOf = {Theta.class, Tau.class}))
private List<Omega> omega; However, I can confirm that the I've added a corresponding Until that is released, you can configure such a A complete example would be: public class ExternalRef319 {
public static void main(String[] args) {
SchemaGeneratorConfigBuilder configBuilder = new SchemaGeneratorConfigBuilder(SchemaVersion.DRAFT_7, OptionPreset.PLAIN_JSON);
configBuilder.with(new Swagger2Module());
configBuilder.forTypesInGeneral().withTypeAttributeOverride((schema, scope, context) -> {
if (scope.getType().getErasedType() == Example.class) {
((ObjectNode) schema.putIfAbsent(context.getKeyword(SchemaKeyword.TAG_PROPERTIES), schema.objectNode()))
.set(context.getKeyword(SchemaKeyword.TAG_SCHEMA),
context.createStandardDefinition(context.getTypeContext().resolve(String.class), null));
}
});
configBuilder.forTypesInGeneral().withCustomDefinitionProvider(new SchemaRefDefinitionProvider());
SchemaGeneratorConfig config = configBuilder.build();
SchemaGenerator generator = new SchemaGenerator(config);
JsonNode jsonSchema = generator.generateSchema(Example.class);
System.out.println(jsonSchema.toPrettyString());
}
private static class SchemaRefDefinitionProvider implements CustomDefinitionProviderV2 {
private Class<?> mainType;
@Override
public CustomDefinition provideCustomSchemaDefinition(ResolvedType javaType, SchemaGenerationContext context) {
Class<?> erasedType = javaType.getErasedType();
if (this.mainType == null) {
this.mainType = erasedType;
}
if (this.mainType == erasedType) {
return null;
}
return Optional.ofNullable(erasedType.getAnnotation(Schema.class))
.map(Schema::ref)
.filter(ref -> !ref.isEmpty())
.map(ref -> context.getGeneratorConfig().createObjectNode()
.put(context.getKeyword(SchemaKeyword.TAG_REF), ref))
.map(schema -> new CustomDefinition(schema,
CustomDefinition.INLINE_DEFINITION,
CustomDefinition.INCLUDING_ATTRIBUTES))
.orElse(null);
}
}
class Example {
@Schema(description = "alpha")
private String alpha;
@Schema(ref = "./BetaSchema.json")
private Object beta;
@Schema(description = "sigma")
private Double sigma;
@ArraySchema(schema = @Schema(oneOf = {Theta.class, Tau.class}))
private List<Omega> omega;
}
class Omega {
}
@Schema(
title = "Theta",
description = "Theta",
additionalProperties = Schema.AdditionalPropertiesValue.FALSE,
ref = "./ThetaSchema.json")
class Theta extends Omega {
}
// Implementation omitted for brevity
@Schema(
title = "Tau",
description = "Tau",
additionalProperties = Schema.AdditionalPropertiesValue.FALSE,
ref = "./TauSchema.json")
class Tau extends Omega {
}
} The output of the above is: {
"$schema" : "http://json-schema.org/draft-07/schema#",
"type" : "object",
"properties" : {
"alpha" : {
"type" : "string",
"description" : "alpha"
},
"beta" : {
"$ref" : "./BetaSchema.json"
},
"omega" : {
"type" : "array",
"items" : {
"type" : "object",
"oneOf" : [ {
"$ref" : "./ThetaSchema.json",
"title" : "Theta",
"description" : "Theta"
}, {
"$ref" : "./TauSchema.json",
"title" : "Tau",
"description" : "Tau"
} ]
}
},
"sigma" : {
"type" : "number",
"description" : "sigma"
},
"$schema" : {
"type" : "string"
}
}
} |
HI @theonepichael, I've just released v4.29.0, i.e., this is now part of the default Now, you just need to change the aforementioned annotation to the following (and update your @ArraySchema(schema = @Schema(oneOf = {Theta.class, Tau.class}))
private List<Omega> omega; |
I would like to add a "$schema" field to the "properties" object on the schema returned by the schema generator. I want to avoid adding this field to the model object classes themselves.
I want this:
To be this:
The text was updated successfully, but these errors were encountered: