From 6c12e6da254ebdcbf43422e3a8ad903fcec4488e Mon Sep 17 00:00:00 2001 From: Heshan Andrews Date: Wed, 29 Apr 2026 17:47:44 +0100 Subject: [PATCH 1/7] Extend validator with custom validation rules --- .../core/schema/CustomValidationRule.java | 10 ++++ .../java/core/schema/ValidationError.java | 2 + .../smithy/java/core/schema/Validator.java | 48 +++++++++++++++++++ 3 files changed, 60 insertions(+) create mode 100644 core/src/main/java/software/amazon/smithy/java/core/schema/CustomValidationRule.java diff --git a/core/src/main/java/software/amazon/smithy/java/core/schema/CustomValidationRule.java b/core/src/main/java/software/amazon/smithy/java/core/schema/CustomValidationRule.java new file mode 100644 index 000000000..978f0c93f --- /dev/null +++ b/core/src/main/java/software/amazon/smithy/java/core/schema/CustomValidationRule.java @@ -0,0 +1,10 @@ +package software.amazon.smithy.java.core.schema; + +import java.util.List; + +public interface CustomValidationRule { + // takes in the schema and checks if the rules applies to it (using schema.getTrait and checking the trait this rule is being defined for is there) + boolean appliesTo(Schema schema); + + List validate(Schema schema, Object value, String path); +} diff --git a/core/src/main/java/software/amazon/smithy/java/core/schema/ValidationError.java b/core/src/main/java/software/amazon/smithy/java/core/schema/ValidationError.java index 37528a5d1..dec81b2a1 100644 --- a/core/src/main/java/software/amazon/smithy/java/core/schema/ValidationError.java +++ b/core/src/main/java/software/amazon/smithy/java/core/schema/ValidationError.java @@ -137,4 +137,6 @@ private static String createMessage(int position) { return "Conflicting list item found at position " + position; } } + + record CustomValidationFailure(String path, Schema schema, String message) implements ValidationError { } } diff --git a/core/src/main/java/software/amazon/smithy/java/core/schema/Validator.java b/core/src/main/java/software/amazon/smithy/java/core/schema/Validator.java index e04d9898e..957f20d44 100644 --- a/core/src/main/java/software/amazon/smithy/java/core/schema/Validator.java +++ b/core/src/main/java/software/amazon/smithy/java/core/schema/Validator.java @@ -11,6 +11,7 @@ import java.time.Instant; import java.util.ArrayList; import java.util.List; +import java.util.ServiceLoader; import java.util.function.BiConsumer; import software.amazon.smithy.java.core.serde.ListSerializer; import software.amazon.smithy.java.core.serde.MapSerializer; @@ -108,6 +109,28 @@ public Builder maxAllowedErrors(int maxAllowedErrors) { } } + static class CustomValidationRuleProvider implements SchemaExtensionProvider> { + private static final SchemaExtensionKey> CUSTOM_VALIDATION_RULE_EXTENSION_KEY = new SchemaExtensionKey<>(); + private static final List CUSTOM_VALIDATION_RULE_LIST = new ArrayList<>(); + + static { + // loading all custom validation rules at once at startup + var loader = ServiceLoader.load(CustomValidationRule.class, CustomValidationRule.class.getClassLoader()); + loader.forEach(rule -> CUSTOM_VALIDATION_RULE_LIST.add(rule)); + } + + @Override + public SchemaExtensionKey> key() { + return CUSTOM_VALIDATION_RULE_EXTENSION_KEY; + } + + @Override + public List provide(Schema schema) { + return CUSTOM_VALIDATION_RULE_LIST + .stream().filter(rule -> rule.appliesTo(schema)).toList(); + } + } + /** * An error that short circuits further validation. */ @@ -224,6 +247,7 @@ public void writeStruct(Schema schema, SerializableStruct struct) { case UNION -> ValidatorOfUnion.validate(this, schema, struct); default -> checkType(schema, ShapeType.STRUCTURE); // this is guaranteed to fail type checking. } + executeCustomValidation(schema, struct); currentSchema = previousSchema; elementCount = previousCount; } @@ -260,6 +284,7 @@ public void writeList(Schema schema, T state, int size, BiConsumer void writeMap(Schema schema, T state, int size, BiConsumer void writeEntry( @Override public void writeBoolean(Schema schema, boolean value) { checkType(schema, ShapeType.BOOLEAN); + executeCustomValidation(schema, value); } @Override public void writeByte(Schema schema, byte value) { checkType(schema, ShapeType.BYTE); validateRange(schema, value, schema.minLongConstraint, schema.maxLongConstraint); + executeCustomValidation(schema, value); } @Override public void writeShort(Schema schema, short value) { checkType(schema, ShapeType.SHORT); validateRange(schema, value, schema.minLongConstraint, schema.maxLongConstraint); + executeCustomValidation(schema, value); } @Override @@ -354,24 +383,28 @@ public void writeInteger(Schema schema, int value) { } default -> checkType(schema, ShapeType.INTEGER); // it's invalid. } + executeCustomValidation(schema, value); } @Override public void writeLong(Schema schema, long value) { checkType(schema, ShapeType.LONG); validateRange(schema, value, schema.minLongConstraint, schema.maxLongConstraint); + executeCustomValidation(schema, value); } @Override public void writeFloat(Schema schema, float value) { checkType(schema, ShapeType.FLOAT); validateRange(schema, value, schema.minDoubleConstraint, schema.maxDoubleConstraint); + executeCustomValidation(schema, value); } @Override public void writeDouble(Schema schema, double value) { checkType(schema, ShapeType.DOUBLE); validateRange(schema, value, schema.minDoubleConstraint, schema.maxDoubleConstraint); + executeCustomValidation(schema, value); } @Override @@ -383,6 +416,7 @@ public void writeBigInteger(Schema schema, BigInteger value) { schema.maxRangeConstraint.toBigInteger()) > 0) { emitRangeError(schema, value); } + executeCustomValidation(schema, value); } @Override @@ -393,6 +427,7 @@ public void writeBigDecimal(Schema schema, BigDecimal value) { } else if (schema.maxRangeConstraint != null && value.compareTo(schema.maxRangeConstraint) > 0) { emitRangeError(schema, value); } + executeCustomValidation(schema, value); } @Override @@ -429,6 +464,7 @@ public void writeString(Schema schema, String value) { } default -> checkType(schema, ShapeType.STRING); // it's invalid, and calling this adds an error. } + executeCustomValidation(schema, value); } @Override @@ -438,16 +474,19 @@ public void writeBlob(Schema schema, ByteBuffer value) { if (length < schema.minLengthConstraint || length > schema.maxLengthConstraint) { addError(new ValidationError.LengthValidationFailure(createPath(), length, schema)); } + executeCustomValidation(schema, value); } @Override public void writeTimestamp(Schema schema, Instant value) { checkType(schema, ShapeType.TIMESTAMP); + executeCustomValidation(schema, value); } @Override public void writeDocument(Schema schema, Document document) { checkType(schema, ShapeType.DOCUMENT); + executeCustomValidation(schema, document); } @Override @@ -488,5 +527,14 @@ private void checkType(Schema schema, ShapeType type) { throw new ValidationShortCircuitException(); } } + + private void executeCustomValidation(Schema schema, Object value) { + var customValidationRules = schema.getExtension(CustomValidationRuleProvider.CUSTOM_VALIDATION_RULE_EXTENSION_KEY); + if (customValidationRules == null) return; + customValidationRules.forEach(rule -> { + var validationErrors = rule.validate(schema, value, createPath()); + validationErrors.forEach(error -> addError(error)); + }); + } } } From e29b0e7c1cde1de4d24b0d9a8960ad0f7c998211 Mon Sep 17 00:00:00 2001 From: Heshan Andrews Date: Fri, 1 May 2026 15:12:50 +0100 Subject: [PATCH 2/7] Fix the initial bugs and make it functional --- .../java/core/schema/CustomValidationRule.java | 6 +++--- .../smithy/java/core/schema/ValidationError.java | 2 +- .../amazon/smithy/java/core/schema/Validator.java | 15 ++++++++++----- ...mithy.java.core.schema.SchemaExtensionProvider | 1 + 4 files changed, 15 insertions(+), 9 deletions(-) create mode 100644 core/src/main/resources/META-INF/services/software.amazon.smithy.java.core.schema.SchemaExtensionProvider diff --git a/core/src/main/java/software/amazon/smithy/java/core/schema/CustomValidationRule.java b/core/src/main/java/software/amazon/smithy/java/core/schema/CustomValidationRule.java index 978f0c93f..da5acf772 100644 --- a/core/src/main/java/software/amazon/smithy/java/core/schema/CustomValidationRule.java +++ b/core/src/main/java/software/amazon/smithy/java/core/schema/CustomValidationRule.java @@ -3,8 +3,8 @@ import java.util.List; public interface CustomValidationRule { - // takes in the schema and checks if the rules applies to it (using schema.getTrait and checking the trait this rule is being defined for is there) - boolean appliesTo(Schema schema); + // takes in the schema and checks if the rules applies to it (using schema.getTrait and checking the trait this rule is being defined for is there) + boolean appliesTo(Schema schema); - List validate(Schema schema, Object value, String path); + List validate(Schema schema, Object value, String path); } diff --git a/core/src/main/java/software/amazon/smithy/java/core/schema/ValidationError.java b/core/src/main/java/software/amazon/smithy/java/core/schema/ValidationError.java index dec81b2a1..a04d03bb6 100644 --- a/core/src/main/java/software/amazon/smithy/java/core/schema/ValidationError.java +++ b/core/src/main/java/software/amazon/smithy/java/core/schema/ValidationError.java @@ -138,5 +138,5 @@ private static String createMessage(int position) { } } - record CustomValidationFailure(String path, Schema schema, String message) implements ValidationError { } + record CustomValidationFailure(String path, Schema schema, String message) implements ValidationError {} } diff --git a/core/src/main/java/software/amazon/smithy/java/core/schema/Validator.java b/core/src/main/java/software/amazon/smithy/java/core/schema/Validator.java index 957f20d44..ac2a6b1f9 100644 --- a/core/src/main/java/software/amazon/smithy/java/core/schema/Validator.java +++ b/core/src/main/java/software/amazon/smithy/java/core/schema/Validator.java @@ -109,10 +109,13 @@ public Builder maxAllowedErrors(int maxAllowedErrors) { } } - static class CustomValidationRuleProvider implements SchemaExtensionProvider> { - private static final SchemaExtensionKey> CUSTOM_VALIDATION_RULE_EXTENSION_KEY = new SchemaExtensionKey<>(); + public static class CustomValidationRuleProvider implements SchemaExtensionProvider> { + private static final SchemaExtensionKey> CUSTOM_VALIDATION_RULE_EXTENSION_KEY = + new SchemaExtensionKey<>(); private static final List CUSTOM_VALIDATION_RULE_LIST = new ArrayList<>(); + public CustomValidationRuleProvider() {} + static { // loading all custom validation rules at once at startup var loader = ServiceLoader.load(CustomValidationRule.class, CustomValidationRule.class.getClassLoader()); @@ -127,7 +130,9 @@ public SchemaExtensionKey> key() { @Override public List provide(Schema schema) { return CUSTOM_VALIDATION_RULE_LIST - .stream().filter(rule -> rule.appliesTo(schema)).toList(); + .stream() + .filter(rule -> rule.appliesTo(schema)) + .toList(); } } @@ -284,7 +289,7 @@ public void writeList(Schema schema, T state, int size, BiConsumer void writeMap(Schema schema, T state, int size, BiConsumer Date: Thu, 7 May 2026 13:08:35 +0100 Subject: [PATCH 3/7] Add a static flag to check custom rule presence --- .../smithy/java/core/schema/Validator.java | 79 +++++++++++-------- 1 file changed, 48 insertions(+), 31 deletions(-) diff --git a/core/src/main/java/software/amazon/smithy/java/core/schema/Validator.java b/core/src/main/java/software/amazon/smithy/java/core/schema/Validator.java index ac2a6b1f9..55384ef39 100644 --- a/core/src/main/java/software/amazon/smithy/java/core/schema/Validator.java +++ b/core/src/main/java/software/amazon/smithy/java/core/schema/Validator.java @@ -109,33 +109,6 @@ public Builder maxAllowedErrors(int maxAllowedErrors) { } } - public static class CustomValidationRuleProvider implements SchemaExtensionProvider> { - private static final SchemaExtensionKey> CUSTOM_VALIDATION_RULE_EXTENSION_KEY = - new SchemaExtensionKey<>(); - private static final List CUSTOM_VALIDATION_RULE_LIST = new ArrayList<>(); - - public CustomValidationRuleProvider() {} - - static { - // loading all custom validation rules at once at startup - var loader = ServiceLoader.load(CustomValidationRule.class, CustomValidationRule.class.getClassLoader()); - loader.forEach(rule -> CUSTOM_VALIDATION_RULE_LIST.add(rule)); - } - - @Override - public SchemaExtensionKey> key() { - return CUSTOM_VALIDATION_RULE_EXTENSION_KEY; - } - - @Override - public List provide(Schema schema) { - return CUSTOM_VALIDATION_RULE_LIST - .stream() - .filter(rule -> rule.appliesTo(schema)) - .toList(); - } - } - /** * An error that short circuits further validation. */ @@ -534,12 +507,56 @@ private void checkType(Schema schema, ShapeType type) { } private void executeCustomValidation(Schema schema, Object value) { + if (!CustomValidationRuleProvider.HAS_CUSTOM_RULE) { + return; + } var customValidationRules = schema.getExtension(CustomValidationRuleProvider.CUSTOM_VALIDATION_RULE_EXTENSION_KEY); - if (customValidationRules == null) return; - customValidationRules.forEach(rule -> { + if (customValidationRules == null) { + return; + } + for (var rule: customValidationRules) { var validationErrors = rule.validate(schema, value, createPath()); - validationErrors.forEach(error -> addError(error)); - }); + for (var error: validationErrors) { + addError(error); + } + } + } + } + + /** + * Registers a new schema extension for a list of {@link CustomValidationRule} + */ + public static class CustomValidationRuleProvider implements SchemaExtensionProvider> { + private static final SchemaExtensionKey> CUSTOM_VALIDATION_RULE_EXTENSION_KEY = + new SchemaExtensionKey<>(); + private static final List CUSTOM_VALIDATION_RULE_LIST = new ArrayList<>(); + private static final boolean HAS_CUSTOM_RULE; + + public CustomValidationRuleProvider() {} + + static { + // loading all custom validation rules at once at startup + var loader = ServiceLoader.load(CustomValidationRule.class, CustomValidationRule.class.getClassLoader()); + for (var customRule: loader) { + CUSTOM_VALIDATION_RULE_LIST.add(customRule); + } + HAS_CUSTOM_RULE = !CUSTOM_VALIDATION_RULE_LIST.isEmpty(); + } + + @Override + public SchemaExtensionKey> key() { + return CUSTOM_VALIDATION_RULE_EXTENSION_KEY; + } + + @Override + public List provide(Schema schema) { + var rulesForThisSchema = new ArrayList(); + for (var rule: CUSTOM_VALIDATION_RULE_LIST) { + if (rule.appliesTo(schema)) { + rulesForThisSchema.add(rule); + } + } + return rulesForThisSchema; } } } From 9c1f9176b0b50f26822c90b149b3e8d4b025018f Mon Sep 17 00:00:00 2001 From: Heshan Andrews Date: Thu, 7 May 2026 13:30:09 +0100 Subject: [PATCH 4/7] Add java docs [using claude] --- .../core/schema/CustomValidationRule.java | 30 ++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/software/amazon/smithy/java/core/schema/CustomValidationRule.java b/core/src/main/java/software/amazon/smithy/java/core/schema/CustomValidationRule.java index da5acf772..2a9679768 100644 --- a/core/src/main/java/software/amazon/smithy/java/core/schema/CustomValidationRule.java +++ b/core/src/main/java/software/amazon/smithy/java/core/schema/CustomValidationRule.java @@ -1,10 +1,38 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + package software.amazon.smithy.java.core.schema; import java.util.List; +/** + * A custom validation rule that extends Smithy's validation framework. + * + *

Implementations are discovered via {@link java.util.ServiceLoader} and must be registered in + * {@code META-INF/services/software.amazon.smithy.java.core.schema.CustomValidationRule}. + * + * @see Validator + * @see Validator.CustomValidationRuleProvider + * @see ValidationError.CustomValidationFailure + */ public interface CustomValidationRule { - // takes in the schema and checks if the rules applies to it (using schema.getTrait and checking the trait this rule is being defined for is there) + /** + * Determines whether this rule applies to the given schema. + * + * @param schema the schema to check + * @return {@code true} if this rule should validate values of this schema + */ boolean appliesTo(Schema schema); + /** + * Validates the given value against this custom rule. + * + * @param schema the schema of the value being validated + * @param value the value to validate + * @param path the path to the value (e.g., "/user/address/zipCode") + * @return a list of validation errors, or an empty list if validation passes + */ List validate(Schema schema, Object value, String path); } From f815d7a1192fb60a49a70b8b266730c629562051 Mon Sep 17 00:00:00 2001 From: Heshan Andrews Date: Thu, 7 May 2026 13:34:42 +0100 Subject: [PATCH 5/7] Rename the validation rules to custom constraints --- ...idationRule.java => CustomConstraint.java} | 8 +-- .../smithy/java/core/schema/Validator.java | 72 +++++++++---------- ...y.java.core.schema.SchemaExtensionProvider | 2 +- 3 files changed, 41 insertions(+), 41 deletions(-) rename core/src/main/java/software/amazon/smithy/java/core/schema/{CustomValidationRule.java => CustomConstraint.java} (85%) diff --git a/core/src/main/java/software/amazon/smithy/java/core/schema/CustomValidationRule.java b/core/src/main/java/software/amazon/smithy/java/core/schema/CustomConstraint.java similarity index 85% rename from core/src/main/java/software/amazon/smithy/java/core/schema/CustomValidationRule.java rename to core/src/main/java/software/amazon/smithy/java/core/schema/CustomConstraint.java index 2a9679768..06d9159a5 100644 --- a/core/src/main/java/software/amazon/smithy/java/core/schema/CustomValidationRule.java +++ b/core/src/main/java/software/amazon/smithy/java/core/schema/CustomConstraint.java @@ -8,16 +8,16 @@ import java.util.List; /** - * A custom validation rule that extends Smithy's validation framework. + * A custom constraint that extends Validation API. * *

Implementations are discovered via {@link java.util.ServiceLoader} and must be registered in - * {@code META-INF/services/software.amazon.smithy.java.core.schema.CustomValidationRule}. + * {@code META-INF/services/software.amazon.smithy.java.core.schema.CustomConstraint}. * * @see Validator - * @see Validator.CustomValidationRuleProvider + * @see Validator.CustomConstraintProvider * @see ValidationError.CustomValidationFailure */ -public interface CustomValidationRule { +public interface CustomConstraint { /** * Determines whether this rule applies to the given schema. * diff --git a/core/src/main/java/software/amazon/smithy/java/core/schema/Validator.java b/core/src/main/java/software/amazon/smithy/java/core/schema/Validator.java index 55384ef39..ec6353b8e 100644 --- a/core/src/main/java/software/amazon/smithy/java/core/schema/Validator.java +++ b/core/src/main/java/software/amazon/smithy/java/core/schema/Validator.java @@ -225,7 +225,7 @@ public void writeStruct(Schema schema, SerializableStruct struct) { case UNION -> ValidatorOfUnion.validate(this, schema, struct); default -> checkType(schema, ShapeType.STRUCTURE); // this is guaranteed to fail type checking. } - executeCustomValidation(schema, struct); + applyCustomConstraints(schema, struct); currentSchema = previousSchema; elementCount = previousCount; } @@ -262,7 +262,7 @@ public void writeList(Schema schema, T state, int size, BiConsumer void writeMap(Schema schema, T state, int size, BiConsumer void writeEntry( @Override public void writeBoolean(Schema schema, boolean value) { checkType(schema, ShapeType.BOOLEAN); - executeCustomValidation(schema, value); + applyCustomConstraints(schema, value); } @Override public void writeByte(Schema schema, byte value) { checkType(schema, ShapeType.BYTE); validateRange(schema, value, schema.minLongConstraint, schema.maxLongConstraint); - executeCustomValidation(schema, value); + applyCustomConstraints(schema, value); } @Override public void writeShort(Schema schema, short value) { checkType(schema, ShapeType.SHORT); validateRange(schema, value, schema.minLongConstraint, schema.maxLongConstraint); - executeCustomValidation(schema, value); + applyCustomConstraints(schema, value); } @Override @@ -361,28 +361,28 @@ public void writeInteger(Schema schema, int value) { } default -> checkType(schema, ShapeType.INTEGER); // it's invalid. } - executeCustomValidation(schema, value); + applyCustomConstraints(schema, value); } @Override public void writeLong(Schema schema, long value) { checkType(schema, ShapeType.LONG); validateRange(schema, value, schema.minLongConstraint, schema.maxLongConstraint); - executeCustomValidation(schema, value); + applyCustomConstraints(schema, value); } @Override public void writeFloat(Schema schema, float value) { checkType(schema, ShapeType.FLOAT); validateRange(schema, value, schema.minDoubleConstraint, schema.maxDoubleConstraint); - executeCustomValidation(schema, value); + applyCustomConstraints(schema, value); } @Override public void writeDouble(Schema schema, double value) { checkType(schema, ShapeType.DOUBLE); validateRange(schema, value, schema.minDoubleConstraint, schema.maxDoubleConstraint); - executeCustomValidation(schema, value); + applyCustomConstraints(schema, value); } @Override @@ -394,7 +394,7 @@ public void writeBigInteger(Schema schema, BigInteger value) { schema.maxRangeConstraint.toBigInteger()) > 0) { emitRangeError(schema, value); } - executeCustomValidation(schema, value); + applyCustomConstraints(schema, value); } @Override @@ -405,7 +405,7 @@ public void writeBigDecimal(Schema schema, BigDecimal value) { } else if (schema.maxRangeConstraint != null && value.compareTo(schema.maxRangeConstraint) > 0) { emitRangeError(schema, value); } - executeCustomValidation(schema, value); + applyCustomConstraints(schema, value); } @Override @@ -442,7 +442,7 @@ public void writeString(Schema schema, String value) { } default -> checkType(schema, ShapeType.STRING); // it's invalid, and calling this adds an error. } - executeCustomValidation(schema, value); + applyCustomConstraints(schema, value); } @Override @@ -452,19 +452,19 @@ public void writeBlob(Schema schema, ByteBuffer value) { if (length < schema.minLengthConstraint || length > schema.maxLengthConstraint) { addError(new ValidationError.LengthValidationFailure(createPath(), length, schema)); } - executeCustomValidation(schema, value); + applyCustomConstraints(schema, value); } @Override public void writeTimestamp(Schema schema, Instant value) { checkType(schema, ShapeType.TIMESTAMP); - executeCustomValidation(schema, value); + applyCustomConstraints(schema, value); } @Override public void writeDocument(Schema schema, Document document) { checkType(schema, ShapeType.DOCUMENT); - executeCustomValidation(schema, document); + applyCustomConstraints(schema, document); } @Override @@ -506,15 +506,15 @@ private void checkType(Schema schema, ShapeType type) { } } - private void executeCustomValidation(Schema schema, Object value) { - if (!CustomValidationRuleProvider.HAS_CUSTOM_RULE) { + private void applyCustomConstraints(Schema schema, Object value) { + if (!CustomConstraintProvider.HAS_CUSTOM_CONSTRAINTS) { return; } - var customValidationRules = schema.getExtension(CustomValidationRuleProvider.CUSTOM_VALIDATION_RULE_EXTENSION_KEY); - if (customValidationRules == null) { + var customConstraints = schema.getExtension(CustomConstraintProvider.CUSTOM_CONSTRAINT_EXTENSION_KEY); + if (customConstraints == null) { return; } - for (var rule: customValidationRules) { + for (var rule: customConstraints) { var validationErrors = rule.validate(schema, value, createPath()); for (var error: validationErrors) { addError(error); @@ -524,34 +524,34 @@ private void executeCustomValidation(Schema schema, Object value) { } /** - * Registers a new schema extension for a list of {@link CustomValidationRule} + * Registers a new schema extension for a list of {@link CustomConstraint} */ - public static class CustomValidationRuleProvider implements SchemaExtensionProvider> { - private static final SchemaExtensionKey> CUSTOM_VALIDATION_RULE_EXTENSION_KEY = + public static class CustomConstraintProvider implements SchemaExtensionProvider> { + private static final SchemaExtensionKey> CUSTOM_CONSTRAINT_EXTENSION_KEY = new SchemaExtensionKey<>(); - private static final List CUSTOM_VALIDATION_RULE_LIST = new ArrayList<>(); - private static final boolean HAS_CUSTOM_RULE; + private static final List CUSTOM_CONSTRAINT_LIST = new ArrayList<>(); + private static final boolean HAS_CUSTOM_CONSTRAINTS; - public CustomValidationRuleProvider() {} + public CustomConstraintProvider() {} static { - // loading all custom validation rules at once at startup - var loader = ServiceLoader.load(CustomValidationRule.class, CustomValidationRule.class.getClassLoader()); + // loading all custom constraints at once at startup + var loader = ServiceLoader.load(CustomConstraint.class, CustomConstraint.class.getClassLoader()); for (var customRule: loader) { - CUSTOM_VALIDATION_RULE_LIST.add(customRule); + CUSTOM_CONSTRAINT_LIST.add(customRule); } - HAS_CUSTOM_RULE = !CUSTOM_VALIDATION_RULE_LIST.isEmpty(); + HAS_CUSTOM_CONSTRAINTS = !CUSTOM_CONSTRAINT_LIST.isEmpty(); } @Override - public SchemaExtensionKey> key() { - return CUSTOM_VALIDATION_RULE_EXTENSION_KEY; + public SchemaExtensionKey> key() { + return CUSTOM_CONSTRAINT_EXTENSION_KEY; } @Override - public List provide(Schema schema) { - var rulesForThisSchema = new ArrayList(); - for (var rule: CUSTOM_VALIDATION_RULE_LIST) { + public List provide(Schema schema) { + var rulesForThisSchema = new ArrayList(); + for (var rule: CUSTOM_CONSTRAINT_LIST) { if (rule.appliesTo(schema)) { rulesForThisSchema.add(rule); } diff --git a/core/src/main/resources/META-INF/services/software.amazon.smithy.java.core.schema.SchemaExtensionProvider b/core/src/main/resources/META-INF/services/software.amazon.smithy.java.core.schema.SchemaExtensionProvider index c5255c16e..3cfbd0e00 100644 --- a/core/src/main/resources/META-INF/services/software.amazon.smithy.java.core.schema.SchemaExtensionProvider +++ b/core/src/main/resources/META-INF/services/software.amazon.smithy.java.core.schema.SchemaExtensionProvider @@ -1 +1 @@ -software.amazon.smithy.java.core.schema.Validator$CustomValidationRuleProvider \ No newline at end of file +software.amazon.smithy.java.core.schema.Validator$CustomConstraintProvider \ No newline at end of file From 1993dbdcdecc2f7fb966ce029b03e96e4d4650c6 Mon Sep 17 00:00:00 2001 From: Heshan Andrews Date: Thu, 7 May 2026 14:17:22 +0100 Subject: [PATCH 6/7] Add intial set of tests --- .../core/schema/TestCustomConstraints.java | 44 +++++++++++++++++++ .../java/core/schema/ValidatorTest.java | 27 ++++++++++++ ...n.smithy.java.core.schema.CustomConstraint | 2 + 3 files changed, 73 insertions(+) create mode 100644 core/src/test/java/software/amazon/smithy/java/core/schema/TestCustomConstraints.java create mode 100644 core/src/test/resources/META-INF/services/software.amazon.smithy.java.core.schema.CustomConstraint diff --git a/core/src/test/java/software/amazon/smithy/java/core/schema/TestCustomConstraints.java b/core/src/test/java/software/amazon/smithy/java/core/schema/TestCustomConstraints.java new file mode 100644 index 000000000..a2c982cea --- /dev/null +++ b/core/src/test/java/software/amazon/smithy/java/core/schema/TestCustomConstraints.java @@ -0,0 +1,44 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.java.core.schema; + +import java.util.List; +import software.amazon.smithy.model.shapes.ShapeType; + +public final class TestCustomConstraints { + + private TestCustomConstraints() {} + + public static class AlwaysFailsConstraint implements CustomConstraint { + @Override + public boolean appliesTo(Schema schema) { + return schema.id().getNamespace().contains("CustomTest"); + } + + @Override + public List validate(Schema schema, Object value, String path) { + return List.of(new ValidationError.CustomValidationFailure( + path, + schema, + "Custom constraint failed")); + } + } + + public static class StringOnlyConstraint implements CustomConstraint { + @Override + public boolean appliesTo(Schema schema) { + return schema.type() == ShapeType.STRING; + } + + @Override + public List validate(Schema schema, Object value, String path) { + return List.of(new ValidationError.CustomValidationFailure( + path, + schema, + "String-only constraint violated")); + } + } +} diff --git a/core/src/test/java/software/amazon/smithy/java/core/schema/ValidatorTest.java b/core/src/test/java/software/amazon/smithy/java/core/schema/ValidatorTest.java index 6bddc4a2f..f6ba7c75e 100644 --- a/core/src/test/java/software/amazon/smithy/java/core/schema/ValidatorTest.java +++ b/core/src/test/java/software/amazon/smithy/java/core/schema/ValidatorTest.java @@ -1281,6 +1281,33 @@ static List validatesRequiredMembersOfBigStructsProvider() { Arguments.of(65, 65, 0, 65)); } + @Test + public void customConstraintsWork() { + var validator = Validator.builder().build(); + var schema = Schema.createInteger(ShapeId.from("smithy.CustomTest#TestInt")); + + var errors = validator.validate(encoder -> encoder.writeInteger(schema, 42)); + + assertThat(errors, hasSize(1)); + var error = errors.get(0); + assertThat(error.path(), equalTo("/")); + assertThat(error.message(), equalTo("Custom constraint failed")); + } + + @Test + public void customConstraintsAreSelective() { + var validator = Validator.builder().build(); + var stringSchema = Schema.createString(ShapeId.from("smithy.CustomTest#SelectiveString")); + + var errors = validator.validate(encoder -> encoder.writeString(stringSchema, "test")); + + assertThat(errors, hasSize(2)); + assertTrue(errors.stream() + .anyMatch(e -> e.message().equals("String-only constraint violated"))); + assertTrue(errors.stream() + .anyMatch(e -> e.message().equals("Custom constraint failed"))); + } + static Schema createBigRequiredSchema(int totalMembers, int requiredCount, int defaultedCount) { var builder = Schema.structureBuilder(ShapeId.from("smithy.example#Foo")); for (var i = 0; i < totalMembers; i++) { diff --git a/core/src/test/resources/META-INF/services/software.amazon.smithy.java.core.schema.CustomConstraint b/core/src/test/resources/META-INF/services/software.amazon.smithy.java.core.schema.CustomConstraint new file mode 100644 index 000000000..a65c1c63e --- /dev/null +++ b/core/src/test/resources/META-INF/services/software.amazon.smithy.java.core.schema.CustomConstraint @@ -0,0 +1,2 @@ +software.amazon.smithy.java.core.schema.TestCustomConstraints$AlwaysFailsConstraint +software.amazon.smithy.java.core.schema.TestCustomConstraints$StringOnlyConstraint From 4a537b13cd8ea7142e07bd9fe3d930f248365061 Mon Sep 17 00:00:00 2001 From: Heshan Andrews Date: Thu, 7 May 2026 14:51:32 +0100 Subject: [PATCH 7/7] Extend the tests to include non primitive shapes --- .../core/schema/TestCustomConstraints.java | 53 +++++++++++++- .../java/core/schema/ValidatorTest.java | 73 +++++++++++++++++++ ...n.smithy.java.core.schema.CustomConstraint | 3 + 3 files changed, 128 insertions(+), 1 deletion(-) diff --git a/core/src/test/java/software/amazon/smithy/java/core/schema/TestCustomConstraints.java b/core/src/test/java/software/amazon/smithy/java/core/schema/TestCustomConstraints.java index a2c982cea..a4eea54e1 100644 --- a/core/src/test/java/software/amazon/smithy/java/core/schema/TestCustomConstraints.java +++ b/core/src/test/java/software/amazon/smithy/java/core/schema/TestCustomConstraints.java @@ -30,7 +30,8 @@ public List validate(Schema schema, Object value, String path) public static class StringOnlyConstraint implements CustomConstraint { @Override public boolean appliesTo(Schema schema) { - return schema.type() == ShapeType.STRING; + return schema.type() == ShapeType.STRING + && schema.id().getNamespace().contains("CustomTest"); } @Override @@ -41,4 +42,54 @@ public List validate(Schema schema, Object value, String path) "String-only constraint violated")); } } + + public static class ListOnlyConstraint implements CustomConstraint { + @Override + public boolean appliesTo(Schema schema) { + return schema.type() == ShapeType.LIST + && schema.id().getNamespace().contains("CustomTest"); + } + + @Override + public List validate(Schema schema, Object value, String path) { + return List.of(new ValidationError.CustomValidationFailure( + path, + schema, + "List-only constraint violated")); + } + } + + public static class StructOnlyConstraint implements CustomConstraint { + + @Override + public boolean appliesTo(Schema schema) { + return schema.type() == ShapeType.STRUCTURE + && schema.id().getNamespace().contains("CustomTest"); + } + + @Override + public List validate(Schema schema, Object value, String path) { + return List.of(new ValidationError.CustomValidationFailure( + path, + schema, + "Struct-only constraint violated")); + } + } + + public static class UnionOnlyConstraint implements CustomConstraint { + + @Override + public boolean appliesTo(Schema schema) { + return schema.type() == ShapeType.UNION + && schema.id().getNamespace().contains("CustomTest"); + } + + @Override + public List validate(Schema schema, Object value, String path) { + return List.of(new ValidationError.CustomValidationFailure( + path, + schema, + "Union-only constraint violated")); + } + } } diff --git a/core/src/test/java/software/amazon/smithy/java/core/schema/ValidatorTest.java b/core/src/test/java/software/amazon/smithy/java/core/schema/ValidatorTest.java index f6ba7c75e..ce0161d4b 100644 --- a/core/src/test/java/software/amazon/smithy/java/core/schema/ValidatorTest.java +++ b/core/src/test/java/software/amazon/smithy/java/core/schema/ValidatorTest.java @@ -31,6 +31,7 @@ import java.util.function.BiConsumer; import java.util.function.BiFunction; import java.util.function.Consumer; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; @@ -1308,6 +1309,78 @@ public void customConstraintsAreSelective() { .anyMatch(e -> e.message().equals("Custom constraint failed"))); } + @Nested + class CustomConstraintsOnNonPrimitiveShapes { + + @Test + void appliesOnStructs() { + var validator = Validator.builder().build(); + var structSchema = Schema.structureBuilder(ShapeId.from("smithy.CustomTest#TestStruct")) + .putMember("value", PreludeSchemas.INTEGER) + .build(); + + var errors = validator.validate(encoder -> { + encoder.writeStruct(structSchema, TestHelper.create(structSchema, (schema, serializer) -> { + serializer.writeInteger(schema.member("value"), 67); + })); + }); + + assertThat(errors, hasSize(3)); + assertTrue(errors.stream().anyMatch(e -> e.path().equals("/") + && e.message().equals("Custom constraint failed"))); + assertTrue(errors.stream().anyMatch(e -> e.path().equals("/") + && e.message().equals("Struct-only constraint violated"))); + assertTrue(errors.stream().anyMatch(e -> e.path().equals("/value") + && e.message().equals("Custom constraint failed"))); + } + + @Test + void appliesOnUnions() { + var validator = Validator.builder().build(); + var unionSchema = Schema.unionBuilder(ShapeId.from("smithy.CustomTest#TestUnion")) + .putMember("stringValue", PreludeSchemas.STRING) + .putMember("intValue", PreludeSchemas.INTEGER) + .build(); + + var errors = validator.validate(encoder -> { + encoder.writeStruct(unionSchema, TestHelper.create(unionSchema, (schema, serializer) -> { + serializer.writeString(schema.member("stringValue"), "value"); + })); + }); + + assertThat(errors, hasSize(4)); + assertTrue(errors.stream().anyMatch(e -> e.path().equals("/") + && e.message().equals("Custom constraint failed"))); + assertTrue(errors.stream().anyMatch(e -> e.path().equals("/") + && e.message().equals("Union-only constraint violated"))); + assertTrue(errors.stream().anyMatch(e -> e.path().equals("/stringValue") + && e.message().equals("Custom constraint failed"))); + assertTrue(errors.stream().anyMatch(e -> e.path().equals("/stringValue") + && e.message().equals("String-only constraint violated"))); + } + + @Test + void appliesOnLists() { + var validator = Validator.builder().build(); + var listSchema = Schema.listBuilder(ShapeId.from("smithy.CustomTest#TestList")) + .putMember("member", PreludeSchemas.INTEGER) + .build(); + + var errors = validator.validate(encoder -> { + encoder.writeList(listSchema, null, 2, (state, serializer) -> { + serializer.writeInteger(PreludeSchemas.INTEGER, 67); + serializer.writeInteger(PreludeSchemas.INTEGER, 67); + }); + }); + + assertThat(errors, hasSize(2)); + assertTrue(errors.stream().anyMatch(e -> e.path().equals("/") + && e.message().equals("Custom constraint failed"))); + assertTrue(errors.stream().anyMatch(e -> e.path().equals("/") + && e.message().equals("List-only constraint violated"))); + } + } + static Schema createBigRequiredSchema(int totalMembers, int requiredCount, int defaultedCount) { var builder = Schema.structureBuilder(ShapeId.from("smithy.example#Foo")); for (var i = 0; i < totalMembers; i++) { diff --git a/core/src/test/resources/META-INF/services/software.amazon.smithy.java.core.schema.CustomConstraint b/core/src/test/resources/META-INF/services/software.amazon.smithy.java.core.schema.CustomConstraint index a65c1c63e..7d1a4e1f7 100644 --- a/core/src/test/resources/META-INF/services/software.amazon.smithy.java.core.schema.CustomConstraint +++ b/core/src/test/resources/META-INF/services/software.amazon.smithy.java.core.schema.CustomConstraint @@ -1,2 +1,5 @@ software.amazon.smithy.java.core.schema.TestCustomConstraints$AlwaysFailsConstraint software.amazon.smithy.java.core.schema.TestCustomConstraints$StringOnlyConstraint +software.amazon.smithy.java.core.schema.TestCustomConstraints$ListOnlyConstraint +software.amazon.smithy.java.core.schema.TestCustomConstraints$StructOnlyConstraint +software.amazon.smithy.java.core.schema.TestCustomConstraints$UnionOnlyConstraint