This is an automated email from the ASF dual-hosted git repository.
jamesbognar pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/juneau.git
The following commit(s) were added to refs/heads/master by this push:
new 9310405cb6 Updates to @Schema annotation and fix build issue.
9310405cb6 is described below
commit 9310405cb62746b66809a176f933fb167a3d0352
Author: James Bognar <[email protected]>
AuthorDate: Mon Oct 13 13:18:13 2025 -0400
Updates to @Schema annotation and fix build issue.
---
.../java/org/apache/juneau/annotation/Schema.java | 579 ++++++++++++++++++++-
.../apache/juneau/annotation/SchemaAnnotation.java | 350 ++++++++++++-
.../org/apache/juneau/httppart/HttpPartSchema.java | 171 +++++-
juneau-docs/docs/release-notes/9.2.0.md | 89 ++++
.../build-overlay/pom.xml | 2 +-
juneau-shaded/juneau-shaded-rest-server/pom.xml | 7 +-
.../juneau/httppart/HttpPartSchema_Body_Test.java | 142 +++++
.../juneau/jsonschema/JsonSchemaGeneratorTest.java | 228 ++++++++
.../annotation/SchemaAnnotation_Test.java | 150 ++++++
9 files changed, 1703 insertions(+), 15 deletions(-)
diff --git
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/annotation/Schema.java
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/annotation/Schema.java
index 9fdbf0cc67..143fce5309 100644
---
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/annotation/Schema.java
+++
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/annotation/Schema.java
@@ -28,13 +28,20 @@ import org.apache.juneau.httppart.*;
import org.apache.juneau.oapi.*;
/**
- * Swagger schema annotation.
+ * Swagger/OpenAPI/JSON Schema annotation.
*
* <p>
* The Schema Object allows the definition of input and output data types.
* These types can be objects, but also primitives and arrays.
- * This object is based on the JSON Schema Specification Draft 4 and uses a
predefined subset of it.
- * On top of this subset, there are extensions provided by this specification
to allow for more complete documentation.
+ * This annotation is based on the JSON Schema Specification and supports
multiple versions:
+ * <ul>
+ * <li><b>JSON Schema Draft 04</b> - Core properties (via Swagger
2.0/OpenAPI 3.0)
+ * <li><b>JSON Schema Draft 2020-12</b> - Extended properties and updated
semantics
+ * </ul>
+ *
+ * <p>
+ * For backward compatibility, all Swagger 2.0 and OpenAPI 3.0 properties are
supported.
+ * New Draft 2020-12 properties are opt-in and can be used alongside existing
properties.
*
* <p>
* Used to populate the auto-generated Swagger documentation and UI for
server-side <ja>@Rest</ja>-annotated classes.
@@ -66,10 +73,23 @@ import org.apache.juneau.oapi.*;
* )
* )
* </p>
+ * <p class='bjava'>
+ * <jc>// Using Draft 2020-12 properties</jc>
+ * <ja>@Schema</ja>(
+ * type=<js>"number"</js>,
+ * exclusiveMaximumValue=<js>"100"</js>, <jc>// Draft 2020-12
numeric format</jc>
+ * examples={<js>"50"</js>, <js>"75"</js>, <js>"99"</js>}, <jc>//
Draft 2020-12 property</jc>
+ * deprecatedProperty=<jk>true</jk> <jc>// Draft 2020-12
property</jc>
+ * )
+ * </p>
*
* <h5 class='section'>See Also:</h5><ul>
* <li class='link'><a class="doclink"
href="https://juneau.apache.org/docs/topics/JuneauBeanSwagger2">juneau-bean-swagger-v2</a>
- * <li class='extlink'><a class="doclink"
href="https://swagger.io/specification/v2#schemaObject">Swagger Schema
Object</a>
+ * <li class='link'><a class="doclink"
href="https://juneau.apache.org/docs/topics/JuneauBeanJsonSchema">juneau-bean-jsonschema</a>
+ * <li class='extlink'><a class="doclink"
href="https://swagger.io/specification/v2#schemaObject">Swagger 2.0 Schema
Object</a>
+ * <li class='extlink'><a class="doclink"
href="https://spec.openapis.org/oas/v3.0.3#schema-object">OpenAPI 3.0 Schema
Object</a>
+ * <li class='extlink'><a class="doclink"
href="https://json-schema.org/draft/2020-12/json-schema-core.html">JSON Schema
Draft 2020-12 Core</a>
+ * <li class='extlink'><a class="doclink"
href="https://json-schema.org/draft/2020-12/json-schema-validation.html">JSON
Schema Draft 2020-12 Validation</a>
* </ul>
*/
@@ -384,6 +404,13 @@ public @interface Schema {
* Only allowed for the following types: <js>"integer"</js>,
<js>"number"</js>.
* <br>If <jk>true</jk>, must be accompanied with <c>maximum</c>.
*
+ * <h5 class='section'>Notes:</h5><ul>
+ * <li class='note'>
+ * <b>Deprecated in JSON Schema Draft 2020-12:</b> This
boolean format is from Swagger 2.0/OpenAPI 3.0 and JSON Schema Draft 04.
+ * Consider using {@link #exclusiveMaximumValue()} for
Draft 2020-12 compliance, which uses a numeric value instead.
+ * For backward compatibility, if {@link
#exclusiveMaximumValue()} is set, it takes precedence over this property.
+ * </ul>
+ *
* <h5 class='section'>Used for:</h5>
* <ul class='spaced-list'>
* <li>
@@ -395,7 +422,9 @@ public @interface Schema {
* </ul>
*
* @return The annotation value.
+ * @deprecated Use {@link #exclusiveMaximumValue()} for JSON Schema
Draft 2020-12 compliance.
*/
+ @Deprecated
boolean exclusiveMaximum() default false;
/**
@@ -413,6 +442,13 @@ public @interface Schema {
* Only allowed for the following types: <js>"integer"</js>,
<js>"number"</js>.
* <br>If <jk>true</jk>, must be accompanied with <c>minimum</c>.
*
+ * <h5 class='section'>Notes:</h5><ul>
+ * <li class='note'>
+ * <b>Deprecated in JSON Schema Draft 2020-12:</b> This
boolean format is from Swagger 2.0/OpenAPI 3.0 and JSON Schema Draft 04.
+ * Consider using {@link #exclusiveMinimumValue()} for
Draft 2020-12 compliance, which uses a numeric value instead.
+ * For backward compatibility, if {@link
#exclusiveMinimumValue()} is set, it takes precedence over this property.
+ * </ul>
+ *
* <h5 class='section'>Used for:</h5>
* <ul class='spaced-list'>
* <li>
@@ -424,7 +460,9 @@ public @interface Schema {
* </ul>
*
* @return The annotation value.
+ * @deprecated Use {@link #exclusiveMinimumValue()} for JSON Schema
Draft 2020-12 compliance.
*/
+ @Deprecated
boolean exclusiveMinimum() default false;
/**
@@ -1239,4 +1277,537 @@ public @interface Schema {
* @return The annotation value.
*/
String[] xml() default {};
+
+
//-----------------------------------------------------------------------------------------------------------------
+ // JSON Schema Draft 2020-12 properties
+
//-----------------------------------------------------------------------------------------------------------------
+
+ /**
+ * <mk>const</mk> field of the JSON Schema.
+ *
+ * <p>
+ * The value of this keyword MAY be of any type, including null.
+ * An instance validates successfully against this keyword if its value
is equal to the value of this keyword.
+ *
+ * <p>
+ * This is a JSON Schema Draft 2020-12 property.
+ *
+ * <h5 class='section'>Examples:</h5>
+ * <p class='bjava'>
+ * <jc>// A constant string value</jc>
+ * <ja>@Schema</ja>(_const=<js>"fixed-value"</js>)
+ * <jk>public</jk> String getStatus() {...}
+ * </p>
+ * <p class='bjava'>
+ * <jc>// A constant numeric value</jc>
+ * <ja>@Schema</ja>(_const=<js>"42"</js>)
+ * <jk>public int</jk> getMagicNumber() {...}
+ * </p>
+ *
+ * <h5 class='section'>Used for:</h5>
+ * <ul class='spaced-list'>
+ * <li>
+ * Server-side schema-based parsing validation.
+ * <li>
+ * Server-side generated Swagger documentation.
+ * <li>
+ * Client-side schema-based serializing validation.
+ * </ul>
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ * <li class='extlink'><a class="doclink"
href="https://json-schema.org/draft/2020-12/json-schema-validation.html#rfc.section.6.1.3">JSON
Schema Validation § 6.1.3</a>
+ * </ul>
+ *
+ * @return The annotation value.
+ */
+ String[] _const() default {};
+
+ /**
+ * <mk>examples</mk> field of the JSON Schema.
+ *
+ * <p>
+ * The value of this keyword MUST be an array.
+ * There are no restrictions on the values within the array.
+ * When multiple examples are applicable, an array of examples can be
used.
+ *
+ * <p>
+ * This is a JSON Schema Draft 2020-12 property.
+ *
+ * <h5 class='section'>Examples:</h5>
+ * <p class='bjava'>
+ * <jc>// Multiple examples of valid values</jc>
+ * <ja>@Schema</ja>(
+ * type=<js>"string"</js>,
+ * examples={<js>"red"</js>, <js>"green"</js>,
<js>"blue"</js>}
+ * )
+ * <jk>public</jk> String getColor() {...}
+ * </p>
+ *
+ * <h5 class='section'>Used for:</h5>
+ * <ul class='spaced-list'>
+ * <li>
+ * Server-side generated Swagger documentation.
+ * </ul>
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ * <li class='extlink'><a class="doclink"
href="https://json-schema.org/draft/2020-12/json-schema-validation.html#rfc.section.9.5">JSON
Schema Validation § 9.5</a>
+ * </ul>
+ *
+ * @return The annotation value.
+ */
+ String[] examples() default {};
+
+ /**
+ * <mk>$comment</mk> field of the JSON Schema.
+ *
+ * <p>
+ * This keyword reserves a location for comments from schema authors to
readers or maintainers of the schema.
+ * The value of this keyword MUST be a string.
+ * Implementations MUST NOT present this string to end users.
+ * Tools for editing schemas SHOULD support displaying and editing this
keyword.
+ *
+ * <p>
+ * This is a JSON Schema Draft 2020-12 property.
+ *
+ * <h5 class='section'>Examples:</h5>
+ * <p class='bjava'>
+ * <ja>@Schema</ja>(
+ * type=<js>"string"</js>,
+ * $comment=<js>"This field is deprecated but maintained
for backward compatibility"</js>
+ * )
+ * <jk>public</jk> String getLegacyField() {...}
+ * </p>
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ * <li class='extlink'><a class="doclink"
href="https://json-schema.org/draft/2020-12/json-schema-core.html#rfc.section.8.3">JSON
Schema Core § 8.3</a>
+ * </ul>
+ *
+ * @return The annotation value.
+ */
+ String[] $comment() default {};
+
+ /**
+ * <mk>deprecated</mk> field of the JSON Schema.
+ *
+ * <p>
+ * The value of this keyword MUST be a boolean.
+ * When true, applications SHOULD refrain from usage of the declared
property.
+ * It may mean the property is going to be removed in the future.
+ *
+ * <p>
+ * This is a JSON Schema Draft 2020-12 property.
+ *
+ * <h5 class='section'>Examples:</h5>
+ * <p class='bjava'>
+ * <ja>@Schema</ja>(deprecated=<jk>true</jk>)
+ * <ja>@Deprecated</ja>
+ * <jk>public</jk> String getOldMethod() {...}
+ * </p>
+ *
+ * <h5 class='section'>Used for:</h5>
+ * <ul class='spaced-list'>
+ * <li>
+ * Server-side generated Swagger documentation.
+ * </ul>
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ * <li class='extlink'><a class="doclink"
href="https://json-schema.org/draft/2020-12/json-schema-validation.html#rfc.section.9.3">JSON
Schema Validation § 9.3</a>
+ * </ul>
+ *
+ * @return The annotation value.
+ */
+ boolean deprecatedProperty() default false;
+
+ /**
+ * <mk>exclusiveMaximum</mk> field of the JSON Schema (Draft 2020-12
numeric value).
+ *
+ * <p>
+ * The value of this keyword MUST be a number.
+ * The instance is valid if it is strictly less than (not equal to) the
value specified by this keyword.
+ *
+ * <p>
+ * This is a JSON Schema Draft 2020-12 property that replaces the
boolean {@link #exclusiveMaximum()}.
+ * For backward compatibility, both properties are supported.
+ * If this property is specified, it takes precedence over the boolean
version.
+ *
+ * <h5 class='section'>Examples:</h5>
+ * <p class='bjava'>
+ * <jc>// A number that must be strictly less than 100</jc>
+ * <ja>@Schema</ja>(
+ * type=<js>"number"</js>,
+ * exclusiveMaximumValue=<js>"100"</js>
+ * )
+ * <jk>public</jk> Double getPercentage() {...}
+ * </p>
+ *
+ * <h5 class='section'>Used for:</h5>
+ * <ul class='spaced-list'>
+ * <li>
+ * Server-side schema-based parsing validation.
+ * <li>
+ * Server-side generated Swagger documentation.
+ * <li>
+ * Client-side schema-based serializing validation.
+ * </ul>
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ * <li class='extlink'><a class="doclink"
href="https://json-schema.org/draft/2020-12/json-schema-validation.html#rfc.section.6.2.3">JSON
Schema Validation § 6.2.3</a>
+ * </ul>
+ *
+ * @return The annotation value.
+ */
+ String exclusiveMaximumValue() default "";
+
+ /**
+ * <mk>exclusiveMinimum</mk> field of the JSON Schema (Draft 2020-12
numeric value).
+ *
+ * <p>
+ * The value of this keyword MUST be a number.
+ * The instance is valid if it is strictly greater than (not equal to)
the value specified by this keyword.
+ *
+ * <p>
+ * This is a JSON Schema Draft 2020-12 property that replaces the
boolean {@link #exclusiveMinimum()}.
+ * For backward compatibility, both properties are supported.
+ * If this property is specified, it takes precedence over the boolean
version.
+ *
+ * <h5 class='section'>Examples:</h5>
+ * <p class='bjava'>
+ * <jc>// A number that must be strictly greater than 0</jc>
+ * <ja>@Schema</ja>(
+ * type=<js>"number"</js>,
+ * exclusiveMinimumValue=<js>"0"</js>
+ * )
+ * <jk>public</jk> Double getPositiveNumber() {...}
+ * </p>
+ *
+ * <h5 class='section'>Used for:</h5>
+ * <ul class='spaced-list'>
+ * <li>
+ * Server-side schema-based parsing validation.
+ * <li>
+ * Server-side generated Swagger documentation.
+ * <li>
+ * Client-side schema-based serializing validation.
+ * </ul>
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ * <li class='extlink'><a class="doclink"
href="https://json-schema.org/draft/2020-12/json-schema-validation.html#rfc.section.6.2.5">JSON
Schema Validation § 6.2.5</a>
+ * </ul>
+ *
+ * @return The annotation value.
+ */
+ String exclusiveMinimumValue() default "";
+
+ /**
+ * <mk>contentMediaType</mk> field of the JSON Schema.
+ *
+ * <p>
+ * If the instance is a string, this property defines the MIME type of
the contents of the string.
+ *
+ * <p>
+ * This is a JSON Schema Draft 2020-12 property.
+ *
+ * <h5 class='section'>Examples:</h5>
+ * <p class='bjava'>
+ * <jc>// A string containing JSON data</jc>
+ * <ja>@Schema</ja>(
+ * type=<js>"string"</js>,
+ * contentMediaType=<js>"application/json"</js>
+ * )
+ * <jk>public</jk> String getJsonData() {...}
+ * </p>
+ *
+ * <h5 class='section'>Used for:</h5>
+ * <ul class='spaced-list'>
+ * <li>
+ * Server-side generated Swagger documentation.
+ * </ul>
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ * <li class='extlink'><a class="doclink"
href="https://json-schema.org/draft/2020-12/json-schema-validation.html#rfc.section.8.3">JSON
Schema Validation § 8.3</a>
+ * </ul>
+ *
+ * @return The annotation value.
+ */
+ String contentMediaType() default "";
+
+ /**
+ * <mk>contentEncoding</mk> field of the JSON Schema.
+ *
+ * <p>
+ * If the instance is a string, this property defines the encoding used
to store the contents.
+ * Common values: "base64", "quoted-printable", "7bit", "8bit", "binary"
+ *
+ * <p>
+ * This is a JSON Schema Draft 2020-12 property.
+ *
+ * <h5 class='section'>Examples:</h5>
+ * <p class='bjava'>
+ * <jc>// A base64-encoded binary string</jc>
+ * <ja>@Schema</ja>(
+ * type=<js>"string"</js>,
+ * contentEncoding=<js>"base64"</js>,
+ * contentMediaType=<js>"image/png"</js>
+ * )
+ * <jk>public</jk> String getImageData() {...}
+ * </p>
+ *
+ * <h5 class='section'>Used for:</h5>
+ * <ul class='spaced-list'>
+ * <li>
+ * Server-side generated Swagger documentation.
+ * </ul>
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ * <li class='extlink'><a class="doclink"
href="https://json-schema.org/draft/2020-12/json-schema-validation.html#rfc.section.8.3">JSON
Schema Validation § 8.3</a>
+ * </ul>
+ *
+ * @return The annotation value.
+ */
+ String contentEncoding() default "";
+
+ /**
+ * <mk>prefixItems</mk> field of the JSON Schema.
+ *
+ * <p>
+ * The value of "prefixItems" MUST be a non-empty array of valid JSON
Schemas.
+ * Validation succeeds if each element of the instance validates
against the schema at the same position, if any.
+ *
+ * <p>
+ * This is a JSON Schema Draft 2020-12 property that provides tuple
validation for arrays.
+ *
+ * <h5 class='section'>Notes:</h5><ul>
+ * <li class='note'>
+ * The format is a <a class="doclink"
href="https://juneau.apache.org/docs/topics/JuneauBeanJsonSchema">juneau-bean-jsonschema</a>
object.
+ * <br>Multiple lines are concatenated with newlines.
+ * </ul>
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ * <li class='extlink'><a class="doclink"
href="https://json-schema.org/draft/2020-12/json-schema-core.html#rfc.section.10.3.1.1">JSON
Schema Core § 10.3.1.1</a>
+ * </ul>
+ *
+ * @return The annotation value.
+ */
+ String[] prefixItems() default {};
+
+ /**
+ * <mk>unevaluatedItems</mk> field of the JSON Schema.
+ *
+ * <p>
+ * The value of "unevaluatedItems" MUST be a valid JSON Schema.
+ * This schema applies to array items that were not evaluated by
"items", "prefixItems", "contains", etc.
+ *
+ * <p>
+ * This is a JSON Schema Draft 2020-12 property.
+ *
+ * <h5 class='section'>Notes:</h5><ul>
+ * <li class='note'>
+ * The format is a <a class="doclink"
href="https://juneau.apache.org/docs/topics/JuneauBeanJsonSchema">juneau-bean-jsonschema</a>
object.
+ * <br>Multiple lines are concatenated with newlines.
+ * </ul>
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ * <li class='extlink'><a class="doclink"
href="https://json-schema.org/draft/2020-12/json-schema-core.html#rfc.section.11.3">JSON
Schema Core § 11.3</a>
+ * </ul>
+ *
+ * @return The annotation value.
+ */
+ String[] unevaluatedItems() default {};
+
+ /**
+ * <mk>unevaluatedProperties</mk> field of the JSON Schema.
+ *
+ * <p>
+ * The value of "unevaluatedProperties" MUST be a valid JSON Schema.
+ * This schema applies to object properties that were not evaluated by
"properties", "patternProperties", etc.
+ *
+ * <p>
+ * This is a JSON Schema Draft 2020-12 property.
+ *
+ * <h5 class='section'>Notes:</h5><ul>
+ * <li class='note'>
+ * The format is a <a class="doclink"
href="https://juneau.apache.org/docs/topics/JuneauBeanJsonSchema">juneau-bean-jsonschema</a>
object.
+ * <br>Multiple lines are concatenated with newlines.
+ * </ul>
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ * <li class='extlink'><a class="doclink"
href="https://json-schema.org/draft/2020-12/json-schema-core.html#rfc.section.11.3">JSON
Schema Core § 11.3</a>
+ * </ul>
+ *
+ * @return The annotation value.
+ */
+ String[] unevaluatedProperties() default {};
+
+ /**
+ * <mk>dependentSchemas</mk> field of the JSON Schema.
+ *
+ * <p>
+ * This keyword specifies subschemas that are evaluated if the instance
is an object and contains a certain property.
+ * The value of this keyword MUST be an object where each value is a
valid JSON Schema.
+ *
+ * <p>
+ * This is a JSON Schema Draft 2020-12 property.
+ *
+ * <h5 class='section'>Notes:</h5><ul>
+ * <li class='note'>
+ * The format is a <a class="doclink"
href="https://juneau.apache.org/docs/topics/JuneauBeanJsonSchema">juneau-bean-jsonschema</a>
object.
+ * <br>Multiple lines are concatenated with newlines.
+ * </ul>
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ * <li class='extlink'><a class="doclink"
href="https://json-schema.org/draft/2020-12/json-schema-core.html#rfc.section.10.2.2.4">JSON
Schema Core § 10.2.2.4</a>
+ * </ul>
+ *
+ * @return The annotation value.
+ */
+ String[] dependentSchemas() default {};
+
+ /**
+ * <mk>dependentRequired</mk> field of the JSON Schema.
+ *
+ * <p>
+ * This keyword specifies properties that are required if a specific
other property is present.
+ * The value of this keyword MUST be an object where each value is an
array of strings.
+ *
+ * <p>
+ * This is a JSON Schema Draft 2020-12 property.
+ *
+ * <h5 class='section'>Notes:</h5><ul>
+ * <li class='note'>
+ * The format is a <a class="doclink"
href="https://juneau.apache.org/docs/topics/JuneauBeanJsonSchema">juneau-bean-jsonschema</a>
object.
+ * <br>Multiple lines are concatenated with newlines.
+ * </ul>
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ * <li class='extlink'><a class="doclink"
href="https://json-schema.org/draft/2020-12/json-schema-validation.html#rfc.section.6.5.4">JSON
Schema Validation § 6.5.4</a>
+ * </ul>
+ *
+ * @return The annotation value.
+ */
+ String[] dependentRequired() default {};
+
+ /**
+ * <mk>if</mk> field of the JSON Schema.
+ *
+ * <p>
+ * This keyword's value MUST be a valid JSON Schema.
+ * This validation outcome of this keyword's subschema has no direct
effect on the overall validation result.
+ * Rather, it controls which of the "then" or "else" keywords are
evaluated.
+ *
+ * <p>
+ * This is a JSON Schema Draft 2020-12 property that provides
conditional schema validation.
+ *
+ * <h5 class='section'>Notes:</h5><ul>
+ * <li class='note'>
+ * The format is a <a class="doclink"
href="https://juneau.apache.org/docs/topics/JuneauBeanJsonSchema">juneau-bean-jsonschema</a>
object.
+ * <br>Multiple lines are concatenated with newlines.
+ * </ul>
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ * <li class='extlink'><a class="doclink"
href="https://json-schema.org/draft/2020-12/json-schema-core.html#rfc.section.10.2.2.3">JSON
Schema Core § 10.2.2.3</a>
+ * </ul>
+ *
+ * @return The annotation value.
+ */
+ String[] _if() default {};
+
+ /**
+ * <mk>then</mk> field of the JSON Schema.
+ *
+ * <p>
+ * This keyword's value MUST be a valid JSON Schema.
+ * When "if" is present, and the instance successfully validates
against its subschema,
+ * then validation succeeds against this keyword if the instance also
successfully validates against this keyword's subschema.
+ *
+ * <p>
+ * This is a JSON Schema Draft 2020-12 property that provides
conditional schema validation.
+ *
+ * <h5 class='section'>Notes:</h5><ul>
+ * <li class='note'>
+ * The format is a <a class="doclink"
href="https://juneau.apache.org/docs/topics/JuneauBeanJsonSchema">juneau-bean-jsonschema</a>
object.
+ * <br>Multiple lines are concatenated with newlines.
+ * </ul>
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ * <li class='extlink'><a class="doclink"
href="https://json-schema.org/draft/2020-12/json-schema-core.html#rfc.section.10.2.2.3">JSON
Schema Core § 10.2.2.3</a>
+ * </ul>
+ *
+ * @return The annotation value.
+ */
+ String[] _then() default {};
+
+ /**
+ * <mk>else</mk> field of the JSON Schema.
+ *
+ * <p>
+ * This keyword's value MUST be a valid JSON Schema.
+ * When "if" is present, and the instance fails to validate against its
subschema,
+ * then validation succeeds against this keyword if the instance
successfully validates against this keyword's subschema.
+ *
+ * <p>
+ * This is a JSON Schema Draft 2020-12 property that provides
conditional schema validation.
+ *
+ * <h5 class='section'>Notes:</h5><ul>
+ * <li class='note'>
+ * The format is a <a class="doclink"
href="https://juneau.apache.org/docs/topics/JuneauBeanJsonSchema">juneau-bean-jsonschema</a>
object.
+ * <br>Multiple lines are concatenated with newlines.
+ * </ul>
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ * <li class='extlink'><a class="doclink"
href="https://json-schema.org/draft/2020-12/json-schema-core.html#rfc.section.10.2.2.3">JSON
Schema Core § 10.2.2.3</a>
+ * </ul>
+ *
+ * @return The annotation value.
+ */
+ String[] _else() default {};
+
+ /**
+ * <mk>$defs</mk> field of the JSON Schema.
+ *
+ * <p>
+ * The "$defs" keyword reserves a location for schema authors to inline
re-usable JSON Schemas into a more general schema.
+ * The value of this keyword MUST be an object. Each value of this
object MUST be a valid JSON Schema.
+ *
+ * <p>
+ * This is a JSON Schema Draft 2020-12 property that replaces the older
"definitions" keyword.
+ *
+ * <h5 class='section'>Notes:</h5><ul>
+ * <li class='note'>
+ * The format is a <a class="doclink"
href="https://juneau.apache.org/docs/topics/JuneauBeanJsonSchema">juneau-bean-jsonschema</a>
object.
+ * <br>Multiple lines are concatenated with newlines.
+ * </ul>
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ * <li class='extlink'><a class="doclink"
href="https://json-schema.org/draft/2020-12/json-schema-core.html#rfc.section.8.2.4">JSON
Schema Core § 8.2.4</a>
+ * </ul>
+ *
+ * @return The annotation value.
+ */
+ String[] $defs() default {};
+
+ /**
+ * <mk>$id</mk> field of the JSON Schema.
+ *
+ * <p>
+ * The "$id" keyword defines a URI for the schema, and the base URI
that other URI references within the schema are resolved against.
+ *
+ * <p>
+ * This is a JSON Schema Draft 2020-12 property.
+ *
+ * <h5 class='section'>Examples:</h5>
+ * <p class='bjava'>
+ * <ja>@Schema</ja>(
+ * $id=<js>"https://example.com/schemas/person.json"</js>,
+ * type=<js>"object"</js>
+ * )
+ * <jk>public class</jk> Person {...}
+ * </p>
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ * <li class='extlink'><a class="doclink"
href="https://json-schema.org/draft/2020-12/json-schema-core.html#rfc.section.8.2.1">JSON
Schema Core § 8.2.1</a>
+ * </ul>
+ *
+ * @return The annotation value.
+ */
+ String $id() default "";
}
diff --git
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/annotation/SchemaAnnotation.java
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/annotation/SchemaAnnotation.java
index 63b3e28729..b966792138 100644
---
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/annotation/SchemaAnnotation.java
+++
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/annotation/SchemaAnnotation.java
@@ -112,8 +112,14 @@ public class SchemaAnnotation {
.appendIf(ne, "discriminator", a.discriminator())
.appendIf(ne, "description", joinnl(a.description(),
a.d()))
.appendFirst(nec, "enum", parseSet(a._enum()),
parseSet(a.e()))
- .appendIf(nf, "exclusiveMaximum", a.exclusiveMaximum()
|| a.emax())
- .appendIf(nf, "exclusiveMinimum", a.exclusiveMinimum()
|| a.emin())
+ // Handle exclusiveMaximum with Draft 2020-12 fallback
+ .appendIf(ne, "exclusiveMaximum",
+ ne.test(a.exclusiveMaximumValue()) ?
a.exclusiveMaximumValue() :
+ (a.exclusiveMaximum() || a.emax()) ? "true" :
null)
+ // Handle exclusiveMinimum with Draft 2020-12 fallback
+ .appendIf(ne, "exclusiveMinimum",
+ ne.test(a.exclusiveMinimumValue()) ?
a.exclusiveMinimumValue() :
+ (a.exclusiveMinimum() || a.emin()) ? "true" :
null)
.appendIf(nem, "externalDocs",
ExternalDocsAnnotation.merge(m.getMap("externalDocs"), a.externalDocs()))
.appendFirst(ne, "format", a.format(), a.f())
.appendIf(ne, "ignore", a.ignore() ? "true" : null)
@@ -136,6 +142,23 @@ public class SchemaAnnotation {
.appendIf(nf, "uniqueItems", a.uniqueItems() || a.ui())
.appendIf(ne, "xml", joinnl(a.xml()))
.appendIf(ne, "$ref", a.$ref())
+ // JSON Schema Draft 2020-12 properties
+ .appendIf(ne, "const", joinnl(a._const()))
+ .appendIf(nec, "examples", a.examples().length == 0 ?
null : Arrays.asList(a.examples()))
+ .appendIf(ne, "$comment", joinnl(a.$comment()))
+ .appendIf(nf, "deprecated", a.deprecatedProperty())
+ .appendIf(ne, "contentMediaType", a.contentMediaType())
+ .appendIf(ne, "contentEncoding", a.contentEncoding())
+ .appendIf(ne, "prefixItems", joinnl(a.prefixItems()))
+ .appendIf(ne, "unevaluatedItems",
joinnl(a.unevaluatedItems()))
+ .appendIf(ne, "unevaluatedProperties",
joinnl(a.unevaluatedProperties()))
+ .appendIf(ne, "dependentSchemas",
joinnl(a.dependentSchemas()))
+ .appendIf(ne, "dependentRequired",
joinnl(a.dependentRequired()))
+ .appendIf(ne, "if", joinnl(a._if()))
+ .appendIf(ne, "then", joinnl(a._then()))
+ .appendIf(ne, "else", joinnl(a._else()))
+ .appendIf(ne, "$defs", joinnl(a.$defs()))
+ .appendIf(ne, "$id", a.$id())
;
}
@@ -188,6 +211,10 @@ public class SchemaAnnotation {
long maxi=-1, maxItems=-1, maxl=-1, maxLength=-1, maxp=-1,
maxProperties=-1, mini=-1, minItems=-1, minl=-1, minLength=-1, minp=-1,
minProperties=-1;
String $ref="", cf="", collectionFormat="", discriminator="",
f="", format="", max="", maximum="", min="", minimum="", mo="", multipleOf="",
p="", pattern="", t="", title="", type="";
String[] _default={}, _enum={}, additionalProperties={},
allOf={}, d={}, description={}, df={}, e={}, properties={}, value={}, xml={};
+ // JSON Schema Draft 2020-12 properties
+ boolean deprecatedProperty;
+ String $id="", contentMediaType="", contentEncoding="",
exclusiveMaximumValue="", exclusiveMinimumValue="";
+ String[] _const={}, examples={}, $comment={}, prefixItems={},
unevaluatedItems={}, unevaluatedProperties={}, dependentSchemas={},
dependentRequired={}, _if={}, _then={}, _else={}, $defs={};
/**
* Constructor.
@@ -810,6 +837,208 @@ public class SchemaAnnotation {
return this;
}
+ //
-----------------------------------------------------------------------------------------------------------------
+ // JSON Schema Draft 2020-12 property setters
+ //
-----------------------------------------------------------------------------------------------------------------
+
+ /**
+ * Sets the {@link Schema#_const} property on this annotation.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ public Builder _const(String...value) {
+ this._const = value;
+ return this;
+ }
+
+ /**
+ * Sets the {@link Schema#examples} property on this annotation.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ public Builder examples(String...value) {
+ this.examples = value;
+ return this;
+ }
+
+ /**
+ * Sets the {@link Schema#$comment} property on this annotation.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ public Builder $comment(String...value) {
+ this.$comment = value;
+ return this;
+ }
+
+ /**
+ * Sets the {@link Schema#deprecatedProperty} property on this
annotation.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ public Builder deprecatedProperty(boolean value) {
+ this.deprecatedProperty = value;
+ return this;
+ }
+
+ /**
+ * Sets the {@link Schema#exclusiveMaximumValue} property on
this annotation.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ public Builder exclusiveMaximumValue(String value) {
+ this.exclusiveMaximumValue = value;
+ return this;
+ }
+
+ /**
+ * Sets the {@link Schema#exclusiveMinimumValue} property on
this annotation.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ public Builder exclusiveMinimumValue(String value) {
+ this.exclusiveMinimumValue = value;
+ return this;
+ }
+
+ /**
+ * Sets the {@link Schema#contentMediaType} property on this
annotation.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ public Builder contentMediaType(String value) {
+ this.contentMediaType = value;
+ return this;
+ }
+
+ /**
+ * Sets the {@link Schema#contentEncoding} property on this
annotation.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ public Builder contentEncoding(String value) {
+ this.contentEncoding = value;
+ return this;
+ }
+
+ /**
+ * Sets the {@link Schema#prefixItems} property on this
annotation.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ public Builder prefixItems(String...value) {
+ this.prefixItems = value;
+ return this;
+ }
+
+ /**
+ * Sets the {@link Schema#unevaluatedItems} property on this
annotation.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ public Builder unevaluatedItems(String...value) {
+ this.unevaluatedItems = value;
+ return this;
+ }
+
+ /**
+ * Sets the {@link Schema#unevaluatedProperties} property on
this annotation.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ public Builder unevaluatedProperties(String...value) {
+ this.unevaluatedProperties = value;
+ return this;
+ }
+
+ /**
+ * Sets the {@link Schema#dependentSchemas} property on this
annotation.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ public Builder dependentSchemas(String...value) {
+ this.dependentSchemas = value;
+ return this;
+ }
+
+ /**
+ * Sets the {@link Schema#dependentRequired} property on this
annotation.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ public Builder dependentRequired(String...value) {
+ this.dependentRequired = value;
+ return this;
+ }
+
+ /**
+ * Sets the {@link Schema#_if} property on this annotation.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ public Builder _if(String...value) {
+ this._if = value;
+ return this;
+ }
+
+ /**
+ * Sets the {@link Schema#_then} property on this annotation.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ public Builder _then(String...value) {
+ this._then = value;
+ return this;
+ }
+
+ /**
+ * Sets the {@link Schema#_else} property on this annotation.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ public Builder _else(String...value) {
+ this._else = value;
+ return this;
+ }
+
+ /**
+ * Sets the {@link Schema#$defs} property on this annotation.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ public Builder $defs(String...value) {
+ this.$defs = value;
+ return this;
+ }
+
+ /**
+ * Sets the {@link Schema#$id} property on this annotation.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ public Builder $id(String value) {
+ this.$id = value;
+ return this;
+ }
+
}
//-----------------------------------------------------------------------------------------------------------------
@@ -824,6 +1053,10 @@ public class SchemaAnnotation {
private final long maxLength, maxl, minLength, minl, maxItems,
maxi, minItems, mini, maxProperties, maxp, minProperties, minp;
private final String $ref, format, f, title, multipleOf, mo,
maximum, max, minimum, min, pattern, p, type, t, collectionFormat, cf,
discriminator;
private final String[] d, _default, df, _enum, e, allOf,
properties, additionalProperties, xml;
+ // JSON Schema Draft 2020-12 fields
+ private final boolean deprecatedProperty;
+ private final String $id, contentMediaType, contentEncoding,
exclusiveMaximumValue, exclusiveMinimumValue;
+ private final String[] _const, examples, $comment, prefixItems,
unevaluatedItems, unevaluatedProperties, dependentSchemas, dependentRequired,
_if, _then, _else, $defs;
Impl(Builder b) {
super(b);
@@ -882,6 +1115,25 @@ public class SchemaAnnotation {
this.ui = b.ui;
this.uniqueItems = b.uniqueItems;
this.xml = copyOf(b.xml);
+ // JSON Schema Draft 2020-12 fields
+ this.deprecatedProperty = b.deprecatedProperty;
+ this.$id = b.$id;
+ this.contentMediaType = b.contentMediaType;
+ this.contentEncoding = b.contentEncoding;
+ this.exclusiveMaximumValue = b.exclusiveMaximumValue;
+ this.exclusiveMinimumValue = b.exclusiveMinimumValue;
+ this._const = copyOf(b._const);
+ this.examples = copyOf(b.examples);
+ this.$comment = copyOf(b.$comment);
+ this.prefixItems = copyOf(b.prefixItems);
+ this.unevaluatedItems = copyOf(b.unevaluatedItems);
+ this.unevaluatedProperties =
copyOf(b.unevaluatedProperties);
+ this.dependentSchemas = copyOf(b.dependentSchemas);
+ this.dependentRequired = copyOf(b.dependentRequired);
+ this._if = copyOf(b._if);
+ this._then = copyOf(b._then);
+ this._else = copyOf(b._else);
+ this.$defs = copyOf(b.$defs);
postConstruct();
}
@@ -1159,6 +1411,100 @@ public class SchemaAnnotation {
public String[] xml() {
return xml;
}
+
+ //
-----------------------------------------------------------------------------------------------------------------
+ // JSON Schema Draft 2020-12 property getters
+ //
-----------------------------------------------------------------------------------------------------------------
+
+ @Override /* Schema */
+ public String[] _const() {
+ return _const;
+ }
+
+ @Override /* Schema */
+ public String[] examples() {
+ return examples;
+ }
+
+ @Override /* Schema */
+ public String[] $comment() {
+ return $comment;
+ }
+
+ @Override /* Schema */
+ public boolean deprecatedProperty() {
+ return deprecatedProperty;
+ }
+
+ @Override /* Schema */
+ public String exclusiveMaximumValue() {
+ return exclusiveMaximumValue;
+ }
+
+ @Override /* Schema */
+ public String exclusiveMinimumValue() {
+ return exclusiveMinimumValue;
+ }
+
+ @Override /* Schema */
+ public String contentMediaType() {
+ return contentMediaType;
+ }
+
+ @Override /* Schema */
+ public String contentEncoding() {
+ return contentEncoding;
+ }
+
+ @Override /* Schema */
+ public String[] prefixItems() {
+ return prefixItems;
+ }
+
+ @Override /* Schema */
+ public String[] unevaluatedItems() {
+ return unevaluatedItems;
+ }
+
+ @Override /* Schema */
+ public String[] unevaluatedProperties() {
+ return unevaluatedProperties;
+ }
+
+ @Override /* Schema */
+ public String[] dependentSchemas() {
+ return dependentSchemas;
+ }
+
+ @Override /* Schema */
+ public String[] dependentRequired() {
+ return dependentRequired;
+ }
+
+ @Override /* Schema */
+ public String[] _if() {
+ return _if;
+ }
+
+ @Override /* Schema */
+ public String[] _then() {
+ return _then;
+ }
+
+ @Override /* Schema */
+ public String[] _else() {
+ return _else;
+ }
+
+ @Override /* Schema */
+ public String[] $defs() {
+ return $defs;
+ }
+
+ @Override /* Schema */
+ public String $id() {
+ return $id;
+ }
}
//-----------------------------------------------------------------------------------------------------------------
diff --git
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/httppart/HttpPartSchema.java
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/httppart/HttpPartSchema.java
index c5593066f1..f1b2a7af10 100644
---
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/httppart/HttpPartSchema.java
+++
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/httppart/HttpPartSchema.java
@@ -654,6 +654,11 @@ public class HttpPartSchema {
boolean noValidate;
Class<? extends HttpPartParser> parser;
Class<? extends HttpPartSerializer> serializer;
+ // JSON Schema Draft 2020-12 properties
+ String _const;
+ String[] examples;
+ Boolean deprecated;
+ Number exclusiveMaximumValue, exclusiveMinimumValue;
/**
* Instantiates a new {@link HttpPartSchema} object based on
the configuration of this builder.
@@ -847,8 +852,23 @@ public class HttpPartSchema {
additionalProperties(HttpPartSchema.toJsonMap(a.additionalProperties()));
allowEmptyValue(a.allowEmptyValue() || a.aev());
collectionFormat(firstNonEmpty(a.collectionFormat(),
a.cf()));
- exclusiveMaximum(a.exclusiveMaximum() || a.emax());
- exclusiveMinimum(a.exclusiveMinimum() || a.emin());
+
+ // Handle exclusiveMaximum with fallback from Draft
2020-12 to Draft 04
+ String exMaxVal = a.exclusiveMaximumValue();
+ if (isNotEmpty(exMaxVal)) {
+ exclusiveMaximumValue(toNumber(exMaxVal));
+ } else if (a.exclusiveMaximum() || a.emax()) {
+ exclusiveMaximum(true);
+ }
+
+ // Handle exclusiveMinimum with fallback from Draft
2020-12 to Draft 04
+ String exMinVal = a.exclusiveMinimumValue();
+ if (isNotEmpty(exMinVal)) {
+ exclusiveMinimumValue(toNumber(exMinVal));
+ } else if (a.exclusiveMinimum() || a.emin()) {
+ exclusiveMinimum(true);
+ }
+
format(firstNonEmpty(a.format(), a.f()));
items(a.items());
maximum(toNumber(a.maximum(), a.max()));
@@ -866,6 +886,12 @@ public class HttpPartSchema {
skipIfEmpty(a.skipIfEmpty() || a.sie());
type(firstNonEmpty(a.type(), a.t()));
uniqueItems(a.uniqueItems() || a.ui());
+
+ // JSON Schema Draft 2020-12 properties
+ _const(joinnlOrNull(a._const()));
+ examples(a.examples());
+ deprecated(a.deprecatedProperty());
+
return this;
}
@@ -3145,6 +3171,102 @@ public class HttpPartSchema {
return this;
}
+ //
-----------------------------------------------------------------------------------------------------------------
+ // JSON Schema Draft 2020-12 property setters
+ //
-----------------------------------------------------------------------------------------------------------------
+
+ /**
+ * <mk>const</mk> field (JSON Schema Draft 2020-12).
+ *
+ * <p>
+ * Defines a constant value for this schema.
+ * The instance must be equal to this value to validate.
+ *
+ * @param value
+ * The new value for this property.
+ * @return This object.
+ */
+ public Builder _const(String value) {
+ this._const = value;
+ return this;
+ }
+
+ /**
+ * <mk>examples</mk> field (JSON Schema Draft 2020-12).
+ *
+ * <p>
+ * An array of example values.
+ * This is used for documentation purposes only and does not
affect validation.
+ *
+ * @param value
+ * The new value for this property.
+ * @return This object.
+ */
+ public Builder examples(String... value) {
+ this.examples = value;
+ return this;
+ }
+
+ /**
+ * <mk>deprecated</mk> field (JSON Schema Draft 2020-12).
+ *
+ * <p>
+ * Indicates that applications should refrain from usage of
this property.
+ * This is used for documentation purposes only and does not
affect validation.
+ *
+ * @param value
+ * The new value for this property.
+ * @return This object.
+ */
+ public Builder deprecated(Boolean value) {
+ deprecated = resolve(value, deprecated);
+ return this;
+ }
+
+ /**
+ * <mk>exclusiveMaximum</mk> field with numeric value (JSON
Schema Draft 2020-12).
+ *
+ * <p>
+ * Defines the exclusive maximum value for numeric types.
+ * The instance is valid if it is strictly less than (not equal
to) this value.
+ *
+ * <p>
+ * This is the Draft 2020-12 version that uses a numeric value
instead of a boolean flag.
+ * If this is set, it takes precedence over the boolean {@link
#exclusiveMaximum(Boolean)} property.
+ *
+ * @param value
+ * The new value for this property.
+ * @return This object.
+ */
+ public Builder exclusiveMaximumValue(Number value) {
+ this.exclusiveMaximumValue = value;
+ return this;
+ }
+
+ /**
+ * <mk>exclusiveMinimum</mk> field with numeric value (JSON
Schema Draft 2020-12).
+ *
+ * <p>
+ * Defines the exclusive minimum value for numeric types.
+ * The instance is valid if it is strictly greater than (not
equal to) this value.
+ *
+ * <p>
+ * This is the Draft 2020-12 version that uses a numeric value
instead of a boolean flag.
+ * If this is set, it takes precedence over the boolean {@link
#exclusiveMinimum(Boolean)} property.
+ *
+ * @param value
+ * The new value for this property.
+ * @return This object.
+ */
+ public Builder exclusiveMinimumValue(Number value) {
+ this.exclusiveMinimumValue = value;
+ return this;
+ }
+
+ //
-----------------------------------------------------------------------------------------------------------------
+ // Other
+ //
-----------------------------------------------------------------------------------------------------------------
+
/**
* Disables Swagger schema usage validation checking.
*
@@ -3227,6 +3349,11 @@ public class HttpPartSchema {
final Class<? extends HttpPartParser> parser;
final Class<? extends HttpPartSerializer> serializer;
final ClassMeta<?> parsedType;
+ // JSON Schema Draft 2020-12 fields
+ final String _const;
+ final String[] examples;
+ final boolean deprecated;
+ final Number exclusiveMaximumValue, exclusiveMinimumValue;
HttpPartSchema(Builder b) {
this.name = b.name;
@@ -3256,6 +3383,12 @@ public class HttpPartSchema {
this.minProperties = b.minProperties;
this.parser = b.parser;
this.serializer = b.serializer;
+ // JSON Schema Draft 2020-12 fields
+ this._const = b._const;
+ this.examples = b.examples;
+ this.deprecated = resolve(b.deprecated);
+ this.exclusiveMaximumValue = b.exclusiveMaximumValue;
+ this.exclusiveMinimumValue = b.exclusiveMinimumValue;
// Calculate parse type
Class<?> parsedType = Object.class;
@@ -3766,6 +3899,8 @@ public class HttpPartSchema {
if (in != null) {
if (! isValidAllowEmpty(in))
throw new SchemaValidationException("Empty
value not allowed.");
+ if (! isValidConst(in))
+ throw new SchemaValidationException("Value does
not match constant. Must be: {0}", _const);
if (! isValidPattern(in))
throw new SchemaValidationException("Value does
not match expected pattern. Must match pattern: {0}", pattern.pattern());
if (! isValidEnum(in))
@@ -3889,6 +4024,20 @@ public class HttpPartSchema {
}
private boolean isValidMinimum(Number x) {
+ // Check Draft 2020-12 exclusiveMinimumValue first (takes
precedence)
+ if (exclusiveMinimumValue != null) {
+ if (x instanceof Integer || x instanceof AtomicInteger)
+ return x.intValue() >
exclusiveMinimumValue.intValue();
+ if (x instanceof Short || x instanceof Byte)
+ return x.shortValue() >
exclusiveMinimumValue.shortValue();
+ if (x instanceof Long || x instanceof AtomicLong || x
instanceof BigInteger)
+ return x.longValue() >
exclusiveMinimumValue.longValue();
+ if (x instanceof Float)
+ return x.floatValue() >
exclusiveMinimumValue.floatValue();
+ if (x instanceof Double || x instanceof BigDecimal)
+ return x.doubleValue() >
exclusiveMinimumValue.doubleValue();
+ }
+ // Fall back to Draft 04 boolean exclusiveMinimum with minimum
if (x instanceof Integer || x instanceof AtomicInteger)
return minimum == null || x.intValue() >
minimum.intValue() || (x.intValue() == minimum.intValue() && (!
exclusiveMinimum));
if (x instanceof Short || x instanceof Byte)
@@ -3903,6 +4052,20 @@ public class HttpPartSchema {
}
private boolean isValidMaximum(Number x) {
+ // Check Draft 2020-12 exclusiveMaximumValue first (takes
precedence)
+ if (exclusiveMaximumValue != null) {
+ if (x instanceof Integer || x instanceof AtomicInteger)
+ return x.intValue() <
exclusiveMaximumValue.intValue();
+ if (x instanceof Short || x instanceof Byte)
+ return x.shortValue() <
exclusiveMaximumValue.shortValue();
+ if (x instanceof Long || x instanceof AtomicLong || x
instanceof BigInteger)
+ return x.longValue() <
exclusiveMaximumValue.longValue();
+ if (x instanceof Float)
+ return x.floatValue() <
exclusiveMaximumValue.floatValue();
+ if (x instanceof Double || x instanceof BigDecimal)
+ return x.doubleValue() <
exclusiveMaximumValue.doubleValue();
+ }
+ // Fall back to Draft 04 boolean exclusiveMaximum with maximum
if (x instanceof Integer || x instanceof AtomicInteger)
return maximum == null || x.intValue() <
maximum.intValue() || (x.intValue() == maximum.intValue() && (!
exclusiveMaximum));
if (x instanceof Short || x instanceof Byte)
@@ -3934,6 +4097,10 @@ public class HttpPartSchema {
return allowEmptyValue || Utils.isNotEmpty(x);
}
+ private boolean isValidConst(String x) {
+ return _const == null || _const.equals(x);
+ }
+
private boolean isValidPattern(String x) {
return pattern == null || pattern.matcher(x).matches();
}
diff --git a/juneau-docs/docs/release-notes/9.2.0.md
b/juneau-docs/docs/release-notes/9.2.0.md
index 0351178875..61bdca01d8 100644
--- a/juneau-docs/docs/release-notes/9.2.0.md
+++ b/juneau-docs/docs/release-notes/9.2.0.md
@@ -16,6 +16,7 @@ Juneau 9.2.0 is a minor release focused on enhancements and
bug fixes.
Major changes include:
- **New Module**: Introduced `juneau-shaded` with five shaded (uber) JAR
artifacts for simplified dependency management, especially useful for Bazel
+- **@Schema Annotation** upgraded to JSON Schema Draft 2020-12 with 18 new
properties, while maintaining full backward compatibility with Draft 04
- JSON Schema beans upgraded to Draft 2020-12 specification with backward
compatibility for Draft 04
- Comprehensive enhancements to HTML5 beans with improved javadocs and
`HtmlBuilder` integration
- Standardized license headers across all Java files
@@ -131,6 +132,78 @@ Major changes include:
### juneau-marshall
+#### @Schema Annotation - JSON Schema Draft 2020-12 Support
+
+- **Draft 2020-12 Compliance**: The `@Schema` annotation has been upgraded to
support JSON Schema Draft 2020-12 while maintaining full backward compatibility
with Draft 04 (used by Swagger 2.0 and OpenAPI 3.0).
+
+ **New Draft 2020-12 Properties**:
+ - `$id()` - Schema URI identifier (e.g.,
`"https://example.com/schemas/user"`)
+ - `_const()` - Constant value validation (value must exactly match)
+ - `examples()` - Array of example values for documentation
+ - `$comment()` - Comments for schema authors (not for end users)
+ - `deprecatedProperty()` - Boolean flag to mark schema/property as deprecated
+ - `exclusiveMaximumValue()` / `exclusiveMinimumValue()` - Numeric exclusive
bounds (replaces boolean flags)
+ - `contentMediaType()` - MIME type for string contents (e.g.,
`"application/json"`)
+ - `contentEncoding()` - Encoding for string contents (e.g., `"base64"`)
+ - `prefixItems()` - Tuple validation for array prefixes
+ - `unevaluatedItems()` - Additional validation for unevaluated array items
+ - `unevaluatedProperties()` - Additional validation for unevaluated object
properties
+ - `dependentSchemas()` - Conditional subschemas based on property presence
+ - `dependentRequired()` - Conditionally required properties
+ - `_if()` / `_then()` / `_else()` - Conditional schema validation
+ - `$defs()` - Reusable schema definitions (replaces `definitions`)
+
+ **Example Usage**:
+ ```java
+ @Schema(
+ type="integer",
+ _const="FIXED_VALUE",
+ examples={"100", "200", "300"},
+ $comment="Internal use only",
+ deprecatedProperty=true,
+ exclusiveMaximumValue="1000",
+ exclusiveMinimumValue="0"
+ )
+ public int legacyField;
+ ```
+
+- **Backward Compatibility**:
+ - Old boolean `exclusiveMaximum` / `exclusiveMinimum` properties still work
but are deprecated
+ - New numeric `exclusiveMaximumValue` / `exclusiveMinimumValue` take
precedence when both are specified
+ - All Draft 04 properties continue to function as before
+
+ **Migration Example**:
+ ```java
+ // Draft 04 style (still works, but deprecated)
+ @Schema(
+ type="integer",
+ exclusiveMaximum=true, // deprecated
+ maximum="100",
+ exclusiveMinimum=true, // deprecated
+ minimum="0"
+ )
+
+ // Draft 2020-12 style (recommended)
+ @Schema(
+ type="integer",
+ exclusiveMaximumValue="100", // 0 < x < 100 (boundaries excluded)
+ exclusiveMinimumValue="0"
+ )
+ ```
+
+- **Enhanced Validation**:
+ - Added `const` validation in `HttpPartSchema` - values must exactly match
the constant
+ - Updated `exclusiveMaximum` / `exclusiveMinimum` validation to support both
boolean flags (Draft 04) and numeric values (Draft 2020-12)
+ - Proper error messages for all new validation types
+
+- **Comprehensive Test Coverage**: Added 38 new unit tests covering:
+ - All 18 new Draft 2020-12 properties
+ - Schema annotation building and processing
+ - JSON schema generation
+ - Runtime validation (input and output)
+ - Backward compatibility scenarios
+ - Precedence rules (new vs old style)
+
#### XML Serialization
- **Text Node Delimiter**: Added `textNodeDelimiter` property to
`XmlSerializer` and `HtmlSerializer` to control spacing between consecutive
text nodes.
@@ -388,6 +461,22 @@ None at this time.
- `juneau-shaded-rest-client` - For REST client work (3.8 MB)
- `juneau-shaded-rest-server` - For REST server work (3.8 MB)
+- **@Schema Annotation**: All existing code using `@Schema` annotations will
continue to work without changes. The upgrade to Draft 2020-12 is fully
backward compatible.
+
+ **Optional Migration** (for new Draft 2020-12 features):
+ - Replace `exclusiveMaximum=true, maximum="100"` with
`exclusiveMaximumValue="100"` for cleaner syntax
+ - Replace `exclusiveMinimum=true, minimum="0"` with
`exclusiveMinimumValue="0"` for cleaner syntax
+ - Use new properties like `_const`, `examples`, `$comment`, etc. as needed
+
+ **Example:**
+ ```java
+ // Old style (still works, but deprecated)
+ @Schema(type="integer", exclusiveMaximum=true, maximum="100",
exclusiveMinimum=true, minimum="0")
+
+ // New style (recommended)
+ @Schema(type="integer", exclusiveMaximumValue="100",
exclusiveMinimumValue="0")
+ ```
+
- **JsonSchema**: Code using `JsonSchema` beans will continue to work without
changes. For Draft 2020-12 features:
- Use `$id` instead of `id` for new schemas
- Use `$defs` instead of `definitions` for new schemas
diff --git a/juneau-examples/juneau-examples-rest-jetty/build-overlay/pom.xml
b/juneau-examples/juneau-examples-rest-jetty/build-overlay/pom.xml
index fd3caab21e..c497f21d54 100644
--- a/juneau-examples/juneau-examples-rest-jetty/build-overlay/pom.xml
+++ b/juneau-examples/juneau-examples-rest-jetty/build-overlay/pom.xml
@@ -60,7 +60,7 @@
<!-- Optional RDF support -->
<dependency>
<groupId>org.apache.juneau</groupId>
- <artifactId>juneau-marshall-rdf</artifactId>
+ <artifactId>juneau-marshall</artifactId>
<version>\${juneau.version}</version>
</dependency>
diff --git a/juneau-shaded/juneau-shaded-rest-server/pom.xml
b/juneau-shaded/juneau-shaded-rest-server/pom.xml
index 40e8acf1b4..c6a0bb95fa 100644
--- a/juneau-shaded/juneau-shaded-rest-server/pom.xml
+++ b/juneau-shaded/juneau-shaded-rest-server/pom.xml
@@ -28,7 +28,7 @@
<artifactId>juneau-shaded-rest-server</artifactId>
<name>Apache Juneau REST Server (Shaded)</name>
<description>
- Shaded JAR containing juneau-core modules plus
juneau-rest-common, juneau-rest-server, juneau-rest-server-rdf, and
juneau-rest-mock.
+ Shaded JAR containing juneau-core modules plus
juneau-rest-common, juneau-rest-server, and juneau-rest-mock.
This artifact provides everything needed for REST server
development with Juneau.
</description>
@@ -51,11 +51,6 @@
<artifactId>juneau-rest-server</artifactId>
<version>${project.version}</version>
</dependency>
- <dependency>
- <groupId>org.apache.juneau</groupId>
- <artifactId>juneau-rest-server-rdf</artifactId>
- <version>${project.version}</version>
- </dependency>
<dependency>
<groupId>org.apache.juneau</groupId>
<artifactId>juneau-rest-mock</artifactId>
diff --git
a/juneau-utest/src/test/java/org/apache/juneau/httppart/HttpPartSchema_Body_Test.java
b/juneau-utest/src/test/java/org/apache/juneau/httppart/HttpPartSchema_Body_Test.java
index cab71d25df..33a40658a1 100644
---
a/juneau-utest/src/test/java/org/apache/juneau/httppart/HttpPartSchema_Body_Test.java
+++
b/juneau-utest/src/test/java/org/apache/juneau/httppart/HttpPartSchema_Body_Test.java
@@ -805,4 +805,146 @@ class HttpPartSchema_Body_Test extends TestBase {
assertThrowsWithMessage(SchemaValidationException.class,
"Maximum number of items exceeded.",
()->s.getItems().getItems().getItems().validateOutput(split("1,2,3,4,5"),
BeanContext.DEFAULT));
assertThrowsWithMessage(SchemaValidationException.class,
"Maximum number of items exceeded.",
()->s.getItems().getItems().getItems().getItems().validateOutput(split("1,2,3,4,5,6"),
BeanContext.DEFAULT));
}
+
+
//-----------------------------------------------------------------------------------------------------------------
+ // JSON Schema Draft 2020-12 validation tests
+
//-----------------------------------------------------------------------------------------------------------------
+
+ @Content
+ @Schema(_const="CONSTANT_VALUE")
+ public static class D01a {}
+
+ @Test void d01a_const_valid() throws Exception {
+ var s = HttpPartSchema.create().applyAll(Content.class,
D01a.class).build();
+ s.validateInput("CONSTANT_VALUE");
+ s.validateInput(null); // null is allowed when not required
+ }
+
+ @Test void d01a_const_invalid() throws Exception {
+ var s = HttpPartSchema.create().applyAll(Content.class,
D01a.class).build();
+ assertThrowsWithMessage(SchemaValidationException.class, "Value
does not match constant. Must be: CONSTANT_VALUE",
()->s.validateInput("OTHER_VALUE"));
+ }
+
+ @Content
+ @Schema(_const="CONSTANT_VALUE", required=true)
+ public static class D01b {}
+
+ @Test void d01b_const_required() throws Exception {
+ var s = HttpPartSchema.create().applyAll(Content.class,
D01b.class).build();
+ s.validateInput("CONSTANT_VALUE");
+ assertThrowsWithMessage(SchemaValidationException.class, "No
value specified.", ()->s.validateInput(null));
+ }
+
+ @Content
+ @Schema(t="integer", exclusiveMaximumValue="100",
exclusiveMinimumValue="0")
+ public static class D02a {}
+
+ @Test void d02a_exclusiveNumericBounds() throws Exception {
+ var s = HttpPartSchema.create().applyAll(Content.class,
D02a.class).build();
+ s.validateOutput(1, BeanContext.DEFAULT);
+ s.validateOutput(50, BeanContext.DEFAULT);
+ s.validateOutput(99, BeanContext.DEFAULT);
+ s.validateOutput(null, BeanContext.DEFAULT);
+ assertThrowsWithMessage(SchemaValidationException.class,
"Minimum value not met.", ()->s.validateOutput(0, BeanContext.DEFAULT));
+ assertThrowsWithMessage(SchemaValidationException.class,
"Maximum value exceeded.", ()->s.validateOutput(100, BeanContext.DEFAULT));
+ assertThrowsWithMessage(SchemaValidationException.class,
"Minimum value not met.", ()->s.validateOutput(-1, BeanContext.DEFAULT));
+ assertThrowsWithMessage(SchemaValidationException.class,
"Maximum value exceeded.", ()->s.validateOutput(101, BeanContext.DEFAULT));
+ }
+
+ @Content
+ @Schema(t="number", exclusiveMaximumValue="10.5",
exclusiveMinimumValue="0.5")
+ public static class D02b {}
+
+ @Test void d02b_exclusiveNumericBounds_doubles() throws Exception {
+ var s = HttpPartSchema.create().applyAll(Content.class,
D02b.class).build();
+ s.validateOutput(0.6, BeanContext.DEFAULT);
+ s.validateOutput(5.0, BeanContext.DEFAULT);
+ s.validateOutput(10.4, BeanContext.DEFAULT);
+ s.validateOutput(null, BeanContext.DEFAULT);
+ assertThrowsWithMessage(SchemaValidationException.class,
"Minimum value not met.", ()->s.validateOutput(0.5, BeanContext.DEFAULT));
+ assertThrowsWithMessage(SchemaValidationException.class,
"Maximum value exceeded.", ()->s.validateOutput(10.5, BeanContext.DEFAULT));
+ assertThrowsWithMessage(SchemaValidationException.class,
"Minimum value not met.", ()->s.validateOutput(0.4, BeanContext.DEFAULT));
+ assertThrowsWithMessage(SchemaValidationException.class,
"Maximum value exceeded.", ()->s.validateOutput(10.6, BeanContext.DEFAULT));
+ }
+
+
//-----------------------------------------------------------------------------------------------------------------
+ // Backward compatibility: Old boolean exclusiveMaximum/exclusiveMinimum
+
//-----------------------------------------------------------------------------------------------------------------
+
+ @Content
+ @Schema(t="integer", exclusiveMaximum=true, exclusiveMinimum=true,
maximum="100", minimum="0")
+ public static class D03a {}
+
+ @Test void d03a_exclusiveBooleanBounds() throws Exception {
+ var s = HttpPartSchema.create().applyAll(Content.class,
D03a.class).build();
+ s.validateOutput(1, BeanContext.DEFAULT);
+ s.validateOutput(50, BeanContext.DEFAULT);
+ s.validateOutput(99, BeanContext.DEFAULT);
+ s.validateOutput(null, BeanContext.DEFAULT);
+ // With boolean flags, 0 and 100 are excluded
+ assertThrowsWithMessage(SchemaValidationException.class,
"Minimum value not met.", ()->s.validateOutput(0, BeanContext.DEFAULT));
+ assertThrowsWithMessage(SchemaValidationException.class,
"Maximum value exceeded.", ()->s.validateOutput(100, BeanContext.DEFAULT));
+ assertThrowsWithMessage(SchemaValidationException.class,
"Minimum value not met.", ()->s.validateOutput(-1, BeanContext.DEFAULT));
+ assertThrowsWithMessage(SchemaValidationException.class,
"Maximum value exceeded.", ()->s.validateOutput(101, BeanContext.DEFAULT));
+ }
+
+ @Content
+ @Schema(t="integer", exclusiveMaximum=false, exclusiveMinimum=false,
maximum="100", minimum="0")
+ public static class D03b {}
+
+ @Test void d03b_inclusiveBounds() throws Exception {
+ var s = HttpPartSchema.create().applyAll(Content.class,
D03b.class).build();
+ // With boolean flags set to false, 0 and 100 are included
+ s.validateOutput(0, BeanContext.DEFAULT);
+ s.validateOutput(1, BeanContext.DEFAULT);
+ s.validateOutput(50, BeanContext.DEFAULT);
+ s.validateOutput(99, BeanContext.DEFAULT);
+ s.validateOutput(100, BeanContext.DEFAULT);
+ s.validateOutput(null, BeanContext.DEFAULT);
+ assertThrowsWithMessage(SchemaValidationException.class,
"Minimum value not met.", ()->s.validateOutput(-1, BeanContext.DEFAULT));
+ assertThrowsWithMessage(SchemaValidationException.class,
"Maximum value exceeded.", ()->s.validateOutput(101, BeanContext.DEFAULT));
+ }
+
+ @Content
+ @Schema(t="integer", exclusiveMaximumValue="100",
exclusiveMinimumValue="0", exclusiveMaximum=false, exclusiveMinimum=false)
+ public static class D03c {}
+
+ @Test void d03c_newStyleTakesPrecedence() throws Exception {
+ var s = HttpPartSchema.create().applyAll(Content.class,
D03c.class).build();
+ // New numeric style should take precedence over old boolean
flags
+ s.validateOutput(1, BeanContext.DEFAULT);
+ s.validateOutput(50, BeanContext.DEFAULT);
+ s.validateOutput(99, BeanContext.DEFAULT);
+ s.validateOutput(null, BeanContext.DEFAULT);
+ assertThrowsWithMessage(SchemaValidationException.class,
"Minimum value not met.", ()->s.validateOutput(0, BeanContext.DEFAULT));
+ assertThrowsWithMessage(SchemaValidationException.class,
"Maximum value exceeded.", ()->s.validateOutput(100, BeanContext.DEFAULT));
+ }
+
+
//-----------------------------------------------------------------------------------------------------------------
+ // Deprecated property (no validation, just ensures it's settable)
+
//-----------------------------------------------------------------------------------------------------------------
+
+ @Content
+ @Schema(deprecatedProperty=true)
+ public static class D04a {}
+
+ @Test void d04a_deprecated() throws Exception {
+ var s = HttpPartSchema.create().applyAll(Content.class,
D04a.class).build();
+ // deprecated is just a flag, doesn't affect validation
+ assertBean(s, "deprecated", "true");
+ }
+
+
//-----------------------------------------------------------------------------------------------------------------
+ // Examples (no validation, documentation only)
+
//-----------------------------------------------------------------------------------------------------------------
+
+ @Content
+ @Schema(examples={"example1", "example2", "example3"})
+ public static class D05a {}
+
+ @Test void d05a_examples() throws Exception {
+ var s = HttpPartSchema.create().applyAll(Content.class,
D05a.class).build();
+ // examples are documentation only, doesn't affect validation
+ assertBean(s, "examples", "[example1,example2,example3]");
+ }
}
\ No newline at end of file
diff --git
a/juneau-utest/src/test/java/org/apache/juneau/jsonschema/JsonSchemaGeneratorTest.java
b/juneau-utest/src/test/java/org/apache/juneau/jsonschema/JsonSchemaGeneratorTest.java
index 2b87065530..828c9f67ba 100755
---
a/juneau-utest/src/test/java/org/apache/juneau/jsonschema/JsonSchemaGeneratorTest.java
+++
b/juneau-utest/src/test/java/org/apache/juneau/jsonschema/JsonSchemaGeneratorTest.java
@@ -1307,4 +1307,232 @@ class JsonSchemaGeneratorTest extends TestBase {
assertContains("$ref", r(x.getSchema(new B())));
}
+
//====================================================================================================
+ // JSON Schema Draft 2020-12 properties
+
//====================================================================================================
+
+ @Test void draft2020_const() throws Exception {
+ var s = JsonSchemaGenerator.DEFAULT.getSession();
+ var schema = s.getSchema(ConstBean.class);
+ assertBean(schema, "const", "MY_CONSTANT");
+ }
+
+ @Schema(type="string", _const="MY_CONSTANT")
+ public static class ConstBean {
+ public String value;
+ }
+
+ @Test void draft2020_examples() throws Exception {
+ var s = JsonSchemaGenerator.DEFAULT.getSession();
+ var schema = s.getSchema(ExamplesBean.class);
+ assertBean(schema, "examples", "[example1,example2,example3]");
+ }
+
+ @Schema(type="string", examples={"example1", "example2", "example3"})
+ public static class ExamplesBean {
+ public String value;
+ }
+
+ @Test void draft2020_deprecated() throws Exception {
+ var s = JsonSchemaGenerator.DEFAULT.getSession();
+ var schema = s.getSchema(DeprecatedBean.class);
+ assertBean(schema, "deprecated", "true");
+ }
+
+ @Schema(type="string", deprecatedProperty=true)
+ public static class DeprecatedBean {
+ public String value;
+ }
+
+ @Test void draft2020_comment() throws Exception {
+ var s = JsonSchemaGenerator.DEFAULT.getSession();
+ var schema = s.getSchema(CommentBean.class);
+ assertBean(schema, "$comment", "This is a comment for
developers");
+ }
+
+ @Schema(type="string", $comment="This is a comment for developers")
+ public static class CommentBean {
+ public String value;
+ }
+
+ @Test void draft2020_exclusiveMaxMinNumeric() throws Exception {
+ var s = JsonSchemaGenerator.DEFAULT.getSession();
+ var schema = s.getSchema(ExclusiveNumericBean.class);
+ assertBean(schema, "exclusiveMaximum,exclusiveMinimum",
"100,0");
+ }
+
+ @Schema(type="integer", exclusiveMaximumValue="100",
exclusiveMinimumValue="0")
+ public static class ExclusiveNumericBean {
+ public int value;
+ }
+
+ @Test void draft2020_contentMediaType() throws Exception {
+ var s = JsonSchemaGenerator.DEFAULT.getSession();
+ var schema = s.getSchema(ContentMediaTypeBean.class);
+ assertBean(schema, "contentMediaType", "application/json");
+ }
+
+ @Schema(type="string", contentMediaType="application/json")
+ public static class ContentMediaTypeBean {
+ public String jsonData;
+ }
+
+ @Test void draft2020_contentEncoding() throws Exception {
+ var s = JsonSchemaGenerator.DEFAULT.getSession();
+ var schema = s.getSchema(ContentEncodingBean.class);
+ assertBean(schema, "contentEncoding", "base64");
+ }
+
+ @Schema(type="string", contentEncoding="base64")
+ public static class ContentEncodingBean {
+ public String encodedData;
+ }
+
+ @Test void draft2020_$id() throws Exception {
+ var s = JsonSchemaGenerator.DEFAULT.getSession();
+ var schema = s.getSchema(IdBean.class);
+ assertBean(schema, "$id",
"https://example.com/schemas/my-schema");
+ }
+
+ @Schema(type="object", $id="https://example.com/schemas/my-schema")
+ public static class IdBean {
+ public String value;
+ }
+
+ @Test void draft2020_$defs() throws Exception {
+ var s = JsonSchemaGenerator.DEFAULT.getSession();
+ var schema = s.getSchema(DefsBean.class);
+ assertBean(schema, "$defs", "MyDef:{type:'string'}");
+ }
+
+ @Schema(type="object", $defs="MyDef:{type:'string'}")
+ public static class DefsBean {
+ public String value;
+ }
+
+ @Test void draft2020_prefixItems() throws Exception {
+ var s = JsonSchemaGenerator.DEFAULT.getSession();
+ var schema = s.getSchema(PrefixItemsBean.class);
+ // prefixItems is stored as newline-separated string, just
verify it exists
+ assertContains("string", schema.get("prefixItems").toString());
+ }
+
+ @Schema(type="array", prefixItems={"string", "number"})
+ public static class PrefixItemsBean {
+ public Object[] tuple;
+ }
+
+ @Test void draft2020_unevaluatedItems() throws Exception {
+ var s = JsonSchemaGenerator.DEFAULT.getSession();
+ var schema = s.getSchema(UnevaluatedItemsBean.class);
+ assertBean(schema, "unevaluatedItems", "false");
+ }
+
+ @Schema(type="array", unevaluatedItems="false")
+ public static class UnevaluatedItemsBean {
+ public Object[] items;
+ }
+
+ @Test void draft2020_unevaluatedProperties() throws Exception {
+ var s = JsonSchemaGenerator.DEFAULT.getSession();
+ var schema = s.getSchema(UnevaluatedPropertiesBean.class);
+ assertBean(schema, "unevaluatedProperties", "false");
+ }
+
+ @Schema(type="object", unevaluatedProperties="false")
+ public static class UnevaluatedPropertiesBean {
+ public String prop1;
+ }
+
+ @Test void draft2020_dependentSchemas() throws Exception {
+ var s = JsonSchemaGenerator.DEFAULT.getSession();
+ var schema = s.getSchema(DependentSchemasBean.class);
+ assertBean(schema, "dependentSchemas", "prop1:{type:'string'}");
+ }
+
+ @Schema(type="object", dependentSchemas="prop1:{type:'string'}")
+ public static class DependentSchemasBean {
+ public String prop1;
+ public String prop2;
+ }
+
+ @Test void draft2020_dependentRequired() throws Exception {
+ var s = JsonSchemaGenerator.DEFAULT.getSession();
+ var schema = s.getSchema(DependentRequiredBean.class);
+ assertBean(schema, "dependentRequired", "prop1:prop2,prop3");
+ }
+
+ @Schema(type="object", dependentRequired="prop1:prop2,prop3")
+ public static class DependentRequiredBean {
+ public String prop1;
+ public String prop2;
+ public String prop3;
+ }
+
+ @Test void draft2020_conditionals() throws Exception {
+ var s = JsonSchemaGenerator.DEFAULT.getSession();
+ var schema = s.getSchema(ConditionalBean.class);
+ assertBean(schema, "if,then,else",
"properties:{foo:{const:'bar'}},required:['baz'],required:['qux']");
+ }
+
+ @Schema(type="object",
+ _if="properties:{foo:{const:'bar'}}",
+ _then="required:['baz']",
+ _else="required:['qux']")
+ public static class ConditionalBean {
+ public String foo;
+ public String baz;
+ public String qux;
+ }
+
+ @Test void draft2020_combinedProperties() throws Exception {
+ var s = JsonSchemaGenerator.DEFAULT.getSession();
+ var schema = s.getSchema(CombinedPropertiesBean.class);
+ assertBean(schema,
"$comment,$id,const,deprecated,examples,exclusiveMaximum,exclusiveMinimum",
+ "A comprehensive
example,https://example.com/combined,FIXED,true,[ex1,ex2],100,0");
+ }
+
+ @Schema(type="integer",
+ $id="https://example.com/combined",
+ _const="FIXED",
+ examples={"ex1", "ex2"},
+ $comment="A comprehensive example",
+ deprecatedProperty=true,
+ exclusiveMaximumValue="100",
+ exclusiveMinimumValue="0")
+ public static class CombinedPropertiesBean {
+ public int value;
+ }
+
+
//====================================================================================================
+ // Backward compatibility: Old boolean exclusiveMaximum/exclusiveMinimum
+
//====================================================================================================
+
+ @Test void backwardCompatibility_exclusiveMaxMin_boolean() throws
Exception {
+ var s = JsonSchemaGenerator.DEFAULT.getSession();
+ var schema = s.getSchema(OldStyleExclusiveBean.class);
+ assertBean(schema,
"exclusiveMaximum,exclusiveMinimum,maximum,minimum", "true,true,100,0");
+ }
+
+ @Schema(type="integer", exclusiveMaximum=true, exclusiveMinimum=true,
maximum="100", minimum="0")
+ public static class OldStyleExclusiveBean {
+ public int value;
+ }
+
+ @Test void backwardCompatibility_newStyleTakesPrecedence() throws
Exception {
+ var s = JsonSchemaGenerator.DEFAULT.getSession();
+ // New numeric style should take precedence in asMap() when
both are set
+ var schema = s.getSchema(MixedStyleExclusiveBean.class);
+ assertBean(schema, "exclusiveMaximum,exclusiveMinimum",
"100,0");
+ }
+
+ @Schema(type="integer",
+ exclusiveMaximumValue="100",
+ exclusiveMinimumValue="0",
+ exclusiveMaximum=false, // This should be ignored in favor of
numeric values
+ exclusiveMinimum=false)
+ public static class MixedStyleExclusiveBean {
+ public int value;
+ }
+
}
\ No newline at end of file
diff --git
a/juneau-utest/src/test/java/org/apache/juneau/jsonschema/annotation/SchemaAnnotation_Test.java
b/juneau-utest/src/test/java/org/apache/juneau/jsonschema/annotation/SchemaAnnotation_Test.java
index 76c1db7744..8fee11265b 100644
---
a/juneau-utest/src/test/java/org/apache/juneau/jsonschema/annotation/SchemaAnnotation_Test.java
+++
b/juneau-utest/src/test/java/org/apache/juneau/jsonschema/annotation/SchemaAnnotation_Test.java
@@ -321,4 +321,154 @@ class SchemaAnnotation_Test extends TestBase {
assertNotEqualsAny(a1.hashCode(), 0, -1);
assertEqualsAll(a1.hashCode(), d1.hashCode(), d2.hashCode());
}
+
+
//------------------------------------------------------------------------------------------------------------------
+ // JSON Schema Draft 2020-12 properties
+
//------------------------------------------------------------------------------------------------------------------
+
+ Schema draft2020_1 = SchemaAnnotation.create()
+ ._const("constantValue")
+ .examples("example1", "example2")
+ .$comment("This is a schema comment")
+ .deprecatedProperty(true)
+ .exclusiveMaximumValue("100")
+ .exclusiveMinimumValue("0")
+ .contentMediaType("application/json")
+ .contentEncoding("base64")
+ .prefixItems("string", "number")
+ .unevaluatedItems("false")
+ .unevaluatedProperties("false")
+ .dependentSchemas("prop1:{type:'string'}")
+ .dependentRequired("prop1:prop2,prop3")
+ ._if("properties:{foo:{const:'bar'}}")
+ ._then("required:['baz']")
+ ._else("required:['qux']")
+ .$defs("MyDef:{type:'string'}")
+ .$id("https://example.com/schemas/my-schema")
+ .build();
+
+ Schema draft2020_2 = SchemaAnnotation.create()
+ ._const("constantValue")
+ .examples("example1", "example2")
+ .$comment("This is a schema comment")
+ .deprecatedProperty(true)
+ .exclusiveMaximumValue("100")
+ .exclusiveMinimumValue("0")
+ .contentMediaType("application/json")
+ .contentEncoding("base64")
+ .prefixItems("string", "number")
+ .unevaluatedItems("false")
+ .unevaluatedProperties("false")
+ .dependentSchemas("prop1:{type:'string'}")
+ .dependentRequired("prop1:prop2,prop3")
+ ._if("properties:{foo:{const:'bar'}}")
+ ._then("required:['baz']")
+ ._else("required:['qux']")
+ .$defs("MyDef:{type:'string'}")
+ .$id("https://example.com/schemas/my-schema")
+ .build();
+
+ @Test void e01_draft2020_basic() {
+ assertBean(draft2020_1,
+
"$comment,$defs,$id,_const,_else,_if,_then,contentEncoding,contentMediaType,dependentRequired,dependentSchemas,deprecatedProperty,examples,exclusiveMaximumValue,exclusiveMinimumValue,prefixItems,unevaluatedItems,unevaluatedProperties",
+ "[This is a schema
comment],[MyDef:{type:'string'}],https://example.com/schemas/my-schema,[constantValue],[required:['qux']],[properties:{foo:{const:'bar'}}],[required:['baz']],base64,application/json,[prop1:prop2,prop3],[prop1:{type:'string'}],true,[example1,example2],100,0,[string,number],[false],[false]");
+ }
+
+ @Test void e02_draft2020_testEquivalency() {
+ assertEquals(draft2020_2, draft2020_1);
+ assertNotEqualsAny(draft2020_1.hashCode(), 0, -1);
+ assertEquals(draft2020_1.hashCode(), draft2020_2.hashCode());
+ }
+
+ @Test void e03_draft2020_testEquivalencyInPropertyStores() {
+ var bc1 = BeanContext.create().annotations(draft2020_1).build();
+ var bc2 = BeanContext.create().annotations(draft2020_2).build();
+ assertSame(bc1, bc2);
+ }
+
+ @Schema(
+ _const="constantValue",
+ examples={"example1", "example2"},
+ $comment="This is a schema comment",
+ deprecatedProperty=true,
+ exclusiveMaximumValue="100",
+ exclusiveMinimumValue="0",
+ contentMediaType="application/json",
+ contentEncoding="base64",
+ prefixItems={"string", "number"},
+ unevaluatedItems="false",
+ unevaluatedProperties="false",
+ dependentSchemas="prop1:{type:'string'}",
+ dependentRequired="prop1:prop2,prop3",
+ _if="properties:{foo:{const:'bar'}}",
+ _then="required:['baz']",
+ _else="required:['qux']",
+ $defs="MyDef:{type:'string'}",
+ $id="https://example.com/schemas/my-schema"
+ )
+ public static class E1 {}
+ Schema e1 = E1.class.getAnnotationsByType(Schema.class)[0];
+
+ @Schema(
+ _const="constantValue",
+ examples={"example1", "example2"},
+ $comment="This is a schema comment",
+ deprecatedProperty=true,
+ exclusiveMaximumValue="100",
+ exclusiveMinimumValue="0",
+ contentMediaType="application/json",
+ contentEncoding="base64",
+ prefixItems={"string", "number"},
+ unevaluatedItems="false",
+ unevaluatedProperties="false",
+ dependentSchemas="prop1:{type:'string'}",
+ dependentRequired="prop1:prop2,prop3",
+ _if="properties:{foo:{const:'bar'}}",
+ _then="required:['baz']",
+ _else="required:['qux']",
+ $defs="MyDef:{type:'string'}",
+ $id="https://example.com/schemas/my-schema"
+ )
+ public static class E2 {}
+ Schema e2 = E2.class.getAnnotationsByType(Schema.class)[0];
+
+ @Test void e04_draft2020_comparisonWithDeclarativeAnnotations() {
+ assertEqualsAll(draft2020_1, e1, e2);
+ assertNotEqualsAny(draft2020_1.hashCode(), 0, -1);
+ assertEqualsAll(draft2020_1.hashCode(), e1.hashCode(),
e2.hashCode());
+ }
+
+
//------------------------------------------------------------------------------------------------------------------
+ // Backward compatibility: exclusiveMaximum/exclusiveMinimum fallback
+
//------------------------------------------------------------------------------------------------------------------
+
+ @Test void f01_backwardCompatibility_exclusiveMaxMin() {
+ // Test that old boolean exclusiveMaximum/exclusiveMinimum
still work
+ Schema oldStyle = SchemaAnnotation.create()
+ .exclusiveMaximum(true)
+ .exclusiveMinimum(true)
+ .maximum("100")
+ .minimum("0")
+ .build();
+
+ assertBean(oldStyle,
"exclusiveMaximum,exclusiveMinimum,maximum,minimum", "true,true,100,0");
+
+ // Test that new numeric style takes precedence
+ Schema newStyle = SchemaAnnotation.create()
+ .exclusiveMaximumValue("100")
+ .exclusiveMinimumValue("0")
+ .build();
+
+ assertBean(newStyle,
"exclusiveMaximumValue,exclusiveMinimumValue", "100,0");
+
+ // Test that new style takes precedence when both are set
+ Schema mixed = SchemaAnnotation.create()
+ .exclusiveMaximum(false)
+ .exclusiveMinimum(false)
+ .exclusiveMaximumValue("100")
+ .exclusiveMinimumValue("0")
+ .build();
+
+ assertBean(mixed,
"exclusiveMaximum,exclusiveMinimum,exclusiveMaximumValue,exclusiveMinimumValue",
"false,false,100,0");
+ }
}
\ No newline at end of file