From 8619825161b9ff420a6374b2ccdc46a51eb2d215 Mon Sep 17 00:00:00 2001 From: Carsten Wickner <11309681+CarstenWickner@users.noreply.github.com> Date: Mon, 30 Oct 2023 07:36:18 +0100 Subject: [PATCH] chore: add example for validation messages (#390) --- .../ValidationErrorMessageExample.java | 152 ++++++++++++++++++ .../jsonschema/examples/ExampleTest.java | 3 +- .../ValidationErrorMessageExample-result.json | 34 ++++ 3 files changed, 188 insertions(+), 1 deletion(-) create mode 100644 jsonschema-examples/src/main/java/com/github/victools/jsonschema/examples/ValidationErrorMessageExample.java create mode 100644 jsonschema-examples/src/test/resources/com/github/victools/jsonschema/examples/ValidationErrorMessageExample-result.json diff --git a/jsonschema-examples/src/main/java/com/github/victools/jsonschema/examples/ValidationErrorMessageExample.java b/jsonschema-examples/src/main/java/com/github/victools/jsonschema/examples/ValidationErrorMessageExample.java new file mode 100644 index 00000000..fa58ef06 --- /dev/null +++ b/jsonschema-examples/src/main/java/com/github/victools/jsonschema/examples/ValidationErrorMessageExample.java @@ -0,0 +1,152 @@ +/* + * Copyright 2023 VicTools. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.github.victools.jsonschema.examples; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.github.victools.jsonschema.generator.MemberScope; +import com.github.victools.jsonschema.generator.OptionPreset; +import com.github.victools.jsonschema.generator.SchemaGenerationContext; +import com.github.victools.jsonschema.generator.SchemaGenerator; +import com.github.victools.jsonschema.generator.SchemaGeneratorConfig; +import com.github.victools.jsonschema.generator.SchemaGeneratorConfigBuilder; +import com.github.victools.jsonschema.generator.SchemaKeyword; +import com.github.victools.jsonschema.generator.SchemaVersion; +import com.github.victools.jsonschema.module.jakarta.validation.JakartaValidationModule; +import com.github.victools.jsonschema.module.jakarta.validation.JakartaValidationOption; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import java.lang.annotation.Annotation; +import java.math.BigDecimal; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Example created in response to #373. + *
+ * Populate custom "message" property in schema based on validation annotations. + *
+ * This is a limited example! Theoretically, all possible validations need to be considered explicitly again. + */ +public class ValidationErrorMessageExample implements SchemaGenerationExampleInterface { + + @Override + public ObjectNode generateSchema() { + SchemaGeneratorConfigBuilder configBuilder = new SchemaGeneratorConfigBuilder(SchemaVersion.DRAFT_2020_12, OptionPreset.PLAIN_JSON) + .with(new JakartaValidationModule(JakartaValidationOption.NOT_NULLABLE_FIELD_IS_REQUIRED)); + configBuilder.forFields() + .withInstanceAttributeOverride(this::includeMessageAttribute); + SchemaGeneratorConfig config = configBuilder.build(); + return new SchemaGenerator(config).generateSchema(TestClass.class); + } + + private > void includeMessageAttribute(ObjectNode collectedMemberAttributes, M member, + SchemaGenerationContext context) { + Map validationMessages = new HashMap<>(); + NotNull notNullAnnotation = this.getAnnotation(member, NotNull.class); + NotEmpty notEmptyAnnotation = this.getAnnotation(member, NotEmpty.class); + NotBlank notBlankAnnotation = this.getAnnotation(member, NotBlank.class); + Size sizeAnnotation = this.getAnnotation(member, Size.class); + Min minAnnotation = this.getAnnotation(member, Min.class); + Max maxAnnotation = this.getAnnotation(member, Max.class); + + if (!member.isFakeContainerItemScope()) { + if (notNullAnnotation != null) { + validationMessages.put(SchemaKeyword.TAG_REQUIRED, notNullAnnotation.message()); + } else if (notEmptyAnnotation != null) { + validationMessages.put(SchemaKeyword.TAG_REQUIRED, notEmptyAnnotation.message()); + } else if (notBlankAnnotation != null) { + validationMessages.put(SchemaKeyword.TAG_REQUIRED, notBlankAnnotation.message()); + } + } + if (member.isContainerType()) { + this.handleArrayValidation(validationMessages, sizeAnnotation, notEmptyAnnotation); + } + if (member.getType().getErasedType() == String.class) { + this.handleStringValidation(validationMessages, sizeAnnotation, notBlankAnnotation, notEmptyAnnotation); + } + if (Number.class.isAssignableFrom(member.getType().getErasedType())) { + this.handleNumberValidation(validationMessages, minAnnotation, maxAnnotation); + } + + if (!validationMessages.isEmpty()) { + ObjectNode messageNode = collectedMemberAttributes.putObject("message"); + validationMessages.forEach((key, message) -> messageNode.put(context.getKeyword(key), + /* resolve message key here before including in schema */ message)); + } + } + + private void handleArrayValidation(Map validationMessages, Size sizeAnnotation, + NotEmpty notEmptyAnnotation) { + if (sizeAnnotation != null && sizeAnnotation.min() > 0) { + validationMessages.put(SchemaKeyword.TAG_ITEMS_MIN, sizeAnnotation.message()); + } else if (notEmptyAnnotation != null) { + validationMessages.put(SchemaKeyword.TAG_ITEMS_MIN, notEmptyAnnotation.message()); + } + if (sizeAnnotation != null && sizeAnnotation.max() < Integer.MAX_VALUE) { + validationMessages.put(SchemaKeyword.TAG_ITEMS_MAX, sizeAnnotation.message()); + } + } + + private void handleStringValidation(Map validationMessages, Size sizeAnnotation, + NotBlank notBlankAnnotation, NotEmpty notEmptyAnnotation) { + if (sizeAnnotation != null && sizeAnnotation.min() > 0) { + validationMessages.put(SchemaKeyword.TAG_LENGTH_MIN, sizeAnnotation.message()); + } else if (notBlankAnnotation != null) { + validationMessages.put(SchemaKeyword.TAG_LENGTH_MIN, notBlankAnnotation.message()); + } else if (notEmptyAnnotation != null) { + validationMessages.put(SchemaKeyword.TAG_LENGTH_MIN, notEmptyAnnotation.message()); + } + if (sizeAnnotation != null && sizeAnnotation.max() < Integer.MAX_VALUE) { + validationMessages.put(SchemaKeyword.TAG_LENGTH_MAX, sizeAnnotation.message()); + } + } + + private void handleNumberValidation(Map validationMessages, Min minAnnotation, Max maxAnnotation) { + if (minAnnotation != null) { + validationMessages.put(SchemaKeyword.TAG_MINIMUM, minAnnotation.message()); + } + if (maxAnnotation != null) { + validationMessages.put(SchemaKeyword.TAG_MAXIMUM, maxAnnotation.message()); + } + } + + private , A extends Annotation> A getAnnotation(M member, Class annotationType) { + if (member.isFakeContainerItemScope()) { + return member.getContainerItemAnnotationConsideringFieldAndGetterIfSupported(annotationType); + } + return member.getAnnotationConsideringFieldAndGetterIfSupported(annotationType); + } + + static class TestClass { + @NotBlank(message = "Mandatory to be present and contain not only whitespaces") + @Size(min = 5, max = 100, message = "Must be between 5 and 100 characters long") + public String title; + + @NotEmpty(message = "Mandatory to be present and contain at least one item") + public List< + @NotNull(message = "No null values in list") + @Min(value = 0, message = "less than 0% is unfair") + @Max(value = 100, message = "more than 100% is unrealistic") + BigDecimal + > results; + } +} diff --git a/jsonschema-examples/src/test/java/com/github/victools/jsonschema/examples/ExampleTest.java b/jsonschema-examples/src/test/java/com/github/victools/jsonschema/examples/ExampleTest.java index 2eb9c30d..92a4782f 100644 --- a/jsonschema-examples/src/test/java/com/github/victools/jsonschema/examples/ExampleTest.java +++ b/jsonschema-examples/src/test/java/com/github/victools/jsonschema/examples/ExampleTest.java @@ -39,7 +39,8 @@ public class ExampleTest { JacksonDescriptionAsTitleExample.class, JacksonSubtypeDefinitionExample.class, StrictTypeInfoExample.class, - SubtypeLookUpExample.class + SubtypeLookUpExample.class, + ValidationErrorMessageExample.class }) public void testExample(Class exampleType) throws Exception { SchemaGenerationExampleInterface exampleImplementation = exampleType.getDeclaredConstructor().newInstance(); diff --git a/jsonschema-examples/src/test/resources/com/github/victools/jsonschema/examples/ValidationErrorMessageExample-result.json b/jsonschema-examples/src/test/resources/com/github/victools/jsonschema/examples/ValidationErrorMessageExample-result.json new file mode 100644 index 00000000..a1e90efc --- /dev/null +++ b/jsonschema-examples/src/test/resources/com/github/victools/jsonschema/examples/ValidationErrorMessageExample-result.json @@ -0,0 +1,34 @@ +{ + "$schema" : "https://json-schema.org/draft/2020-12/schema", + "type" : "object", + "properties" : { + "results" : { + "minItems" : 1, + "message" : { + "required" : "Mandatory to be present and contain at least one item", + "minItems" : "Mandatory to be present and contain at least one item" + }, + "type" : "array", + "items" : { + "type" : "number", + "minimum" : 0, + "maximum" : 100, + "message" : { + "minimum" : "less than 0% is unfair", + "maximum" : "more than 100% is unrealistic" + } + } + }, + "title" : { + "type" : "string", + "minLength" : 5, + "maxLength" : 100, + "message" : { + "required" : "Mandatory to be present and contain not only whitespaces", + "maxLength" : "Must be between 5 and 100 characters long", + "minLength" : "Must be between 5 and 100 characters long" + } + } + }, + "required" : [ "results", "title" ] +} \ No newline at end of file