From 6d0be0652b7126b24b06f3db0d6d0d9819831d84 Mon Sep 17 00:00:00 2001 From: Fahad Zubair Date: Mon, 18 Aug 2025 12:26:09 +0100 Subject: [PATCH 1/6] RFC: Custom Validation --- design/src/rfcs/rfc0047_custom_validation.md | 79 ++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 design/src/rfcs/rfc0047_custom_validation.md diff --git a/design/src/rfcs/rfc0047_custom_validation.md b/design/src/rfcs/rfc0047_custom_validation.md new file mode 100644 index 00000000000..8f9f679be51 --- /dev/null +++ b/design/src/rfcs/rfc0047_custom_validation.md @@ -0,0 +1,79 @@ +RFC: Custom Validation Exception +================================ + +> Status: RFC +> +> Applies to: server + +For a summarized list of proposed changes, see the [Changes Checklist](#changes-checklist) section. + +This RFC defines a mechanism to use custom `ValidationException` instead of `smithy.framework#ValidationException`, enabling service teams to use a validation exception that they might have already published to the external world or maybe they are porting an existing non-smithy based model to smithy and need backward compatibility. + +Terminology +----------- + +- **Constrained shape**: a shape that is either: +1. a shape with a [constraint trait][https://awslabs.github.io/smithy/2.0/spec/constraint-traits.html] attached +2. a (member) shape with a [required trait] attached +3. an [enum shape] +4. an [intEnum shape] +5. a [structure shape] with at least one required member shape; or +6. a shape whose closure includes any of the above. +- **ValidationException**: A Smithy shape that should be serialized on the wire in case the required field is not present. +- **Shape closure**: +- **::std::hash::Hash**: + +The user experience if this RFC is implemented +---------------------------------------------- + +If there is a constrained shape in the input shape closure of an operation, then the operation must add a `ValidationException` to the operation' error list, otherwise an error is raised. + +``` +Caused by: ValidationResult(shouldAbort=true, messages=[LogMessage(level=SEVERE, message=Operation com.aws.example#GetStorage takes in input that is constrained (https://awslabs.github.io/smithy/2.0/spec/constraint-traits.html), and as such can fail with a validation exception. You must model this behavior in the operation shape in your model file. +```smithy +use smithy.framework#ValidationException + +operation GetStorage { + ... + errors: [..., ValidationException] // <-- Add this. +} +```)]) +``` + +The solution is to add the `ValidationException` in the errors list: +``` +operation SomeOperation { + # <...input / output definiton..> + errors: [ + # <...other fields...> + ValidationException + ] +} +``` + +Once this RF is implemented, service developers will be able to: + +1. **Mark a shape to be used as ValidationException** by applying the `@validationException` trait to any structure shape that is marked with `@error` trait. + +```smithy +@validationException +@error +structure CustomValidationException { +} +``` + +2. There **must** be a String member shape field that is marked as the **message field** using the `@validationMessage` trait. + +3. For now, the structure must be default constructible. If it has any required shape those must be + + +How to actually implement this RFC +---------------------------------- + +In order to implement this feature, we need to add X and update Y... + + +Changes checklist +----------------- + +- [x] Create new struct `NewFeature` From 00562a6943569ba55bce0fcf42f3cfd126909cce Mon Sep 17 00:00:00 2001 From: Fahad Zubair Date: Mon, 18 Aug 2025 13:38:27 +0100 Subject: [PATCH 2/6] changes to RFC --- design/src/rfcs/rfc0047_custom_validation.md | 414 ++++++++++++++++++- 1 file changed, 397 insertions(+), 17 deletions(-) diff --git a/design/src/rfcs/rfc0047_custom_validation.md b/design/src/rfcs/rfc0047_custom_validation.md index 8f9f679be51..0a504dcfa0a 100644 --- a/design/src/rfcs/rfc0047_custom_validation.md +++ b/design/src/rfcs/rfc0047_custom_validation.md @@ -1,4 +1,54 @@ -RFC: Custom Validation Exception + +> Status: RFC +> +> Applies to: server + +For a summarized list of proposed changes, see the [Changes Checklist](#changes-checklist) section. + +This RFC defines a mechanism to use custom `ValidationException` instead of `smithy.framework#ValidationException`, enabling service teams to use a validation exception that they might have already published to the external world or maybe they are porting an existing non-smithy based model to smithy and need backward compatibility. There are some server developers who have requested a way to pass a list of failing fields to the client with error messages for use cases where they would like to display and make use of the errors to show on the user interface . + +Terminology +----------- + +- **Constrained shape**: a shape that is either: +1. a shape with a [constraint trait][https://awslabs.github.io/smithy/2.0/spec/constraint-traits.html] attached +2. a (member) shape with a [required trait] attached +3. an [enum shape] +4. an [intEnum shape] +5. a [structure shape] with at least one required member shape; or +6. a shape whose closure includes any of the above. +- **ValidationException**: A Smithy shape that should be serialized on the wire in case the required field is not present. +- **Shape closure**: +- **::std::hash::Hash**: +RFC: Custom Validation Exceptions + +> Status: RFC +> +> Applies to: server + +For a summarized list of proposed changes, see the [Changes Checklist](#changes-checklist) section. + +This RFC defines a mechanism to use custom validation exception shapes instead of the standard `smithy.framework#ValidationException`, enabling service teams to maintain backward compatibility with existing APIs or use validation exceptions that have already been published to external consumers. This addresses scenarios where service teams are migrating from non-Smithy models or need to provide validation error responses in a format that differs from the standard Smithy validation exception structure. + +Terminology +----------- + +- **Constrained shape**: a shape that is either: + 1. a shape with a [constraint trait] attached + 2. a (member) shape with a [`required` trait] attached + 3. an [`enum` shape] + 4. an [`intEnum` shape] + 5. a [`structure` shape] with at least one required member shape; or + 6. a shape whose closure includes any of the above. +- **ValidationException**: A Smithy error shape that is serialized in the response when constraint validation fails during request processing. +- **Shape closure**: the set of shapes a shape can "reach", including itself. +- **Custom validation exception**: A user-defined error shape marked with validation-specific traits that replaces the standard `smithy.framework#ValidationException`. + +[constraint trait]: https://smithy.io/2.0/spec/constraint-traits.html +[`required` trait]: https://smithy.io/2.0/spec/type-refinement-traits.html#required-trait +[`enum` shape]: https://smithy.io/2.0/spec/simple-types.html#enum +[`intEnum` shape]: https://smithy.io/2.0/spec/simple-types.html#intenum +[`structure` shape]: https://smithy.io/2.0/spec/aggregate-types.html#structure ================================ > Status: RFC @@ -7,7 +57,7 @@ RFC: Custom Validation Exception For a summarized list of proposed changes, see the [Changes Checklist](#changes-checklist) section. -This RFC defines a mechanism to use custom `ValidationException` instead of `smithy.framework#ValidationException`, enabling service teams to use a validation exception that they might have already published to the external world or maybe they are porting an existing non-smithy based model to smithy and need backward compatibility. +This RFC defines a mechanism to use custom `ValidationException` instead of `smithy.framework#ValidationException`, enabling service teams to use a validation exception that they might have already published to the external world or maybe they are porting an existing non-smithy based model to smithy and need backward compatibility. There are some server developers who have requested a way to pass a list of failing fields to the client with error messages for use cases where they would like to display and make use of the errors to show on the user interface . Terminology ----------- @@ -26,10 +76,10 @@ Terminology The user experience if this RFC is implemented ---------------------------------------------- -If there is a constrained shape in the input shape closure of an operation, then the operation must add a `ValidationException` to the operation' error list, otherwise an error is raised. +Currently, if there is a constrained shape in the input shape closure of an operation, the operation **must** add `smithy.framework#ValidationException` to the operation's error list, otherwise a build error is raised: ``` -Caused by: ValidationResult(shouldAbort=true, messages=[LogMessage(level=SEVERE, message=Operation com.aws.example#GetStorage takes in input that is constrained (https://awslabs.github.io/smithy/2.0/spec/constraint-traits.html), and as such can fail with a validation exception. You must model this behavior in the operation shape in your model file. +Caused by: ValidationResult(shouldAbort=true, messages=[LogMessage(level=SEVERE, message=Operation com.aws.example#GetStorage takes in input that is constrained (https://smithy.io/2.0/spec/constraint-traits.html), and as such can fail with a validation exception. You must model this behavior in the operation shape in your model file. ```smithy use smithy.framework#ValidationException @@ -40,40 +90,370 @@ operation GetStorage { ```)]) ``` -The solution is to add the `ValidationException` in the errors list: -``` +The current solution requires adding the standard `ValidationException` to the errors list: + +```smithy operation SomeOperation { - # <...input / output definiton..> + // <...input / output definition...> errors: [ - # <...other fields...> + // <...other errors...> ValidationException ] } ``` -Once this RF is implemented, service developers will be able to: +### Problems with the current approach + +Service teams face several challenges with the mandatory use of `smithy.framework#ValidationException`: + +1. **Backward compatibility**: Teams migrating existing APIs to Smithy cannot maintain their existing validation error format +2. **Published APIs**: Teams that have already published validation exception schemas to external consumers cannot change the response format without breaking clients +3. **Custom error handling**: Teams may need additional fields or different field names for their validation errors +4. **Legacy system integration**: Teams integrating with existing systems that expect specific validation error formats + +### Solution: Custom validation exception traits + +Once this RFC is implemented, service developers will be able to define custom validation exceptions using the following approach: + +#### 1. Define a custom validation exception shape + +Apply the `@validationException` trait to any structure shape that is also marked with the `@error` trait: + +```smithy +@validationException +@error("client") +structure CustomValidationException { + // Structure members defined below +} +``` + +#### 2. Specify the message field (required) + +The custom validation exception **must** have exactly one String member marked with the `@validationMessage` trait to serve as the primary error message: + +```smithy +@validationException +@error("client") +structure CustomValidationException { + @validationMessage + @required + message: String +} +``` + +#### 3. Default constructibility requirement + +For the initial implementation, the custom validation exception structure **must** be default constructible. This means the shape either: + +a. **must not** contain any constrained shapes that the framework cannot construct; or +b. any constrained shapes **must** have default values specified + +```smithy +@validationException +@error("client") +structure CustomValidationException { + @validationMessage + @required + message: String, + + @default("VALIDATION_ERROR") + errorCode: String, + + @default("ErrorInValidation") + errorKind: ErrorKind +} + +enum ErrorKind { + SomeOtherError, + ErrorInValidation +} +``` + +#### 4. Optional field list support + +Optionally, the custom validation exception **may** include a field marked with `@validationFieldList` to provide detailed information about which fields failed validation. This field can be one of: -1. **Mark a shape to be used as ValidationException** by applying the `@validationException` trait to any structure shape that is marked with `@error` trait. +a. A String shape (for simple field name listing) +b. A List shape where the member is a String shape (for multiple field names) +c. A List shape where the member is a structure shape with detailed field information + +For option (c), the structure shape: +- **must** have a String member marked with `@validationFieldName` +- **may** have a String member marked with `@validationFieldMessage` ```smithy @validationException -@error +@error("client") structure CustomValidationException { + @validationMessage + @required + message: String, + + @default("VALIDATION_ERROR") + errorCode: String, + + @validationFieldList + fieldErrors: ValidationFieldList +} + +list ValidationFieldList { + member: ValidationField +} + +structure ValidationField { + @validationFieldName + @required + fieldName: String, + + @validationFieldMessage + @required + errorMessage: String } ``` -2. There **must** be a String member shape field that is marked as the **message field** using the `@validationMessage` trait. +#### 5. Using custom validation exceptions in operations + +Replace `smithy.framework#ValidationException` with your custom validation exception in operation error lists: + +```smithy +operation GetUser { + input: GetUserInput, + output: GetUserOutput, + errors: [ + CustomValidationException, // Instead of ValidationException + UserNotFoundError + ] +} +``` + +#### 6. Future extensibility + +In a future iteration, the default constructibility requirement (rule #3) **may** be relaxed by allowing developers to register a factory function on the service builder. This factory function would be called by the framework whenever it needs to instantiate the custom validation exception, providing access to: + +- The operation shape information +- The request context +- The specific constraint violations that occurred + +```rust +// Future API (not part of this RFC) +let service = MyService::builder() + .validation_exception_factory(|operation, request, violations| { + CustomValidationException { + message: format!("Validation failed for operation {}", operation.name()), + error_code: determine_error_code(&violations), + field_errors: map_violations_to_fields(violations), + } + }) + .build(); +``` + +### Additional use cases + +This RFC addresses several additional scenarios beyond basic backward compatibility: + +1. **Multi-language service teams**: Teams with clients in multiple programming languages may need validation errors in a format that's easier to parse in specific languages +2. **UI-focused applications**: Frontend applications may require structured field-level errors for form validation display +3. **Monitoring and analytics**: Teams may need additional metadata in validation errors for monitoring, logging, or analytics purposes +4. **Compliance requirements**: Some domains may have regulatory requirements for specific error message formats +5. **Internationalization**: Teams may need to include locale information or error codes that map to localized messages + +### Backwards compatibility + +This feature is entirely opt-in and maintains full backward compatibility: + +**Non-breaking changes:** +- Existing services using `smithy.framework#ValidationException` continue to work unchanged +- No changes to existing APIs or behavior for services without custom validation exception traits -3. For now, the structure must be default constructible. If it has any required shape those must be +**Breaking changes:** +- **Adding custom validation exception traits to an existing service is a breaking change** for the API contract +- Clients expecting the standard `ValidationException` format will receive the new custom format +- Service teams must coordinate with client teams when migrating to custom validation exceptions - How to actually implement this RFC ---------------------------------- -In order to implement this feature, we need to add X and update Y... +### 1. Create validation exception traits + +**Location**: `codegen-server-traits/src/main/resources/META-INF/smithy/validation-exception.smithy` + +Define the new traits in the smithy-rs server traits namespace: + +```smithy +$version: "2.0" + +namespace smithy.rust.codegen.server.traits + +/// Marks a structure as a custom validation exception that can replace +/// smithy.framework#ValidationException in operation error lists. +@trait(selector: "structure[trait|error]") +structure validationException {} + +/// Marks a String member as the primary message field for a validation exception. +/// Exactly one member in a @validationException structure must have this trait. +@trait(selector: "structure[trait|smithy.rust.codegen.server.traits#validationException] > member[target=smithy.api#String]") +structure validationMessage {} + +/// Marks a member as containing the list of field-level validation errors. +/// The target shape must be a String, List, or List where +/// the structure contains validation field information. +@trait(selector: "structure[trait|smithy.rust.codegen.server.traits#validationException] > member") +structure validationFieldList {} + +/// Marks a String member as containing the field name in a validation field structure. +@trait(selector: "structure > member[target=smithy.api#String]") +structure validationFieldName {} + +/// Marks a String member as containing the field error message in a validation field structure. +@trait(selector: "structure > member[target=smithy.api#String]") +structure validationFieldMessage {} +``` + +### 2. Validation logic + +**Location**: `codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/validators/` + +Add validation to ensure custom validation exceptions are properly defined: + +```kotlin +class CustomValidationExceptionValidator : Validator { + override fun validate(model: Model): List { + val events = mutableListOf() + + model.shapes(StructureShape::class.java) + .filter { it.hasTrait(ValidationExceptionTrait::class.java) } + .forEach { shape -> + // Validate that the shape also has @error trait + if (!shape.hasTrait(ErrorTrait::class.java)) { + events.add(ValidationEvent.builder() + .id("CustomValidationException.MissingErrorTrait") + .severity(Severity.ERROR) + .shape(shape) + .message("@validationException requires @error trait") + .build()) + } + + // Validate exactly one @validationMessage field + val messageFields = shape.members().values + .filter { it.hasTrait(ValidationMessageTrait::class.java) } + + when (messageFields.size) { + 0 -> events.add(ValidationEvent.builder() + .id("CustomValidationException.MissingMessageField") + .severity(Severity.ERROR) + .shape(shape) + .message("@validationException requires exactly one @validationMessage field") + .build()) + 1 -> { /* Valid */ } + else -> events.add(ValidationEvent.builder() + .id("CustomValidationException.MultipleMessageFields") + .severity(Severity.ERROR) + .shape(shape) + .message("@validationException can have only one @validationMessage field") + .build()) + } + + // Validate default constructibility + validateDefaultConstructibility(shape, model, events) + } + + return events + } +} +``` + +### 3. Code generation modifications + +**Location**: `codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/` + +Modify the constraint violation handling to detect and use custom validation exceptions: + +```kotlin +class CustomValidationExceptionGenerator( + private val model: Model, + private val symbolProvider: SymbolProvider, + private val rustCrate: RustCrate +) { + fun generateCustomValidationExceptionSupport(serviceShape: ServiceShape) { + val customValidationExceptions = findCustomValidationExceptions(serviceShape) + + customValidationExceptions.forEach { (operation, customException) -> + generateValidationExceptionMapper(operation, customException) + } + } + + private fun generateValidationExceptionMapper( + operation: OperationShape, + customException: StructureShape + ) { + val operationName = symbolProvider.toSymbol(operation).name + val exceptionSymbol = symbolProvider.toSymbol(customException) + + rustCrate.withModule(RustModule.private("validation_mappers")) { + rust(""" + pub(crate) fn map_constraint_violations_to_${operationName.toSnakeCase()}( + violations: crate::constrained::ConstraintViolations + ) -> ${exceptionSymbol.rustType().render()} { + ${generateMappingLogic(customException, violations)} + } + """) + } + } +} +``` + +### 4. Framework integration + +**Location**: `codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/protocol/` + +Update protocol generators to use custom validation exceptions when available: + +```kotlin +// In the appropriate protocol generator +private fun generateConstraintViolationHandling(operation: OperationShape): Writable { + val customValidationException = findCustomValidationException(operation) + + return if (customValidationException != null) { + writable { + rust(""" + match constraint_violations { + Ok(input) => input, + Err(violations) => { + let custom_exception = crate::validation_mappers::map_constraint_violations_to_${operation.id.name.toSnakeCase()}(violations); + return Err(${customValidationException.name}(custom_exception).into()); + } + } + """) + } + } else { + // Use standard ValidationException + generateStandardValidationExceptionHandling() + } +} +``` + +### 5. Testing strategy + +Comprehensive testing is required to ensure the feature works correctly: + +1. **Trait validation tests**: Ensure custom validation exception traits are properly validated +2. **Code generation tests**: Verify correct Rust code is generated for various custom validation exception configurations +3. **Integration tests**: Test end-to-end validation exception handling with custom exceptions +4. **Backward compatibility tests**: Ensure existing services continue to work unchanged +5. **Error message tests**: Verify custom validation exceptions contain expected field information - Changes checklist ----------------- -- [x] Create new struct `NewFeature` +- [ ] Create `validationException`, `validationMessage`, `validationFieldList`, `validationFieldName`, and `validationFieldMessage` traits in `codegen-server-traits` +- [ ] Implement `CustomValidationExceptionValidator` to validate proper usage of custom validation exception traits +- [ ] Create `CustomValidationExceptionGenerator` to generate mapping logic from constraint violations to custom exceptions +- [ ] Modify protocol generators to detect and use custom validation exceptions instead of standard `ValidationException` +- [ ] Update constraint violation handling in server request processing to use custom validation exception mappers +- [ ] Generate default constructors for custom validation exception shapes +- [ ] Add comprehensive unit tests for trait validation logic +- [ ] Add integration tests for end-to-end custom validation exception handling +- [ ] Create documentation explaining custom validation exception usage and migration strategies +- [ ] Add examples showing various custom validation exception patterns +- [ ] Update existing constraint violation documentation to mention custom validation exceptions +- [ ] Ensure backward compatibility with existing services using standard `ValidationException` From 0e2fccb2c2863fba2d260ef68cd97b491a096721 Mon Sep 17 00:00:00 2001 From: Fahad Zubair Date: Mon, 18 Aug 2025 18:38:01 +0100 Subject: [PATCH 3/6] Changes to RFC --- design/src/rfcs/rfc0047_custom_validation.md | 253 +++++++++---------- 1 file changed, 126 insertions(+), 127 deletions(-) diff --git a/design/src/rfcs/rfc0047_custom_validation.md b/design/src/rfcs/rfc0047_custom_validation.md index 0a504dcfa0a..22de2720392 100644 --- a/design/src/rfcs/rfc0047_custom_validation.md +++ b/design/src/rfcs/rfc0047_custom_validation.md @@ -1,26 +1,5 @@ - -> Status: RFC -> -> Applies to: server - -For a summarized list of proposed changes, see the [Changes Checklist](#changes-checklist) section. - -This RFC defines a mechanism to use custom `ValidationException` instead of `smithy.framework#ValidationException`, enabling service teams to use a validation exception that they might have already published to the external world or maybe they are porting an existing non-smithy based model to smithy and need backward compatibility. There are some server developers who have requested a way to pass a list of failing fields to the client with error messages for use cases where they would like to display and make use of the errors to show on the user interface . - -Terminology ------------ - -- **Constrained shape**: a shape that is either: -1. a shape with a [constraint trait][https://awslabs.github.io/smithy/2.0/spec/constraint-traits.html] attached -2. a (member) shape with a [required trait] attached -3. an [enum shape] -4. an [intEnum shape] -5. a [structure shape] with at least one required member shape; or -6. a shape whose closure includes any of the above. -- **ValidationException**: A Smithy shape that should be serialized on the wire in case the required field is not present. -- **Shape closure**: -- **::std::hash::Hash**: -RFC: Custom Validation Exceptions +RFC: Custom Validation Exception +================================ > Status: RFC > @@ -49,29 +28,6 @@ Terminology [`enum` shape]: https://smithy.io/2.0/spec/simple-types.html#enum [`intEnum` shape]: https://smithy.io/2.0/spec/simple-types.html#intenum [`structure` shape]: https://smithy.io/2.0/spec/aggregate-types.html#structure -================================ - -> Status: RFC -> -> Applies to: server - -For a summarized list of proposed changes, see the [Changes Checklist](#changes-checklist) section. - -This RFC defines a mechanism to use custom `ValidationException` instead of `smithy.framework#ValidationException`, enabling service teams to use a validation exception that they might have already published to the external world or maybe they are porting an existing non-smithy based model to smithy and need backward compatibility. There are some server developers who have requested a way to pass a list of failing fields to the client with error messages for use cases where they would like to display and make use of the errors to show on the user interface . - -Terminology ------------ - -- **Constrained shape**: a shape that is either: -1. a shape with a [constraint trait][https://awslabs.github.io/smithy/2.0/spec/constraint-traits.html] attached -2. a (member) shape with a [required trait] attached -3. an [enum shape] -4. an [intEnum shape] -5. a [structure shape] with at least one required member shape; or -6. a shape whose closure includes any of the above. -- **ValidationException**: A Smithy shape that should be serialized on the wire in case the required field is not present. -- **Shape closure**: -- **::std::hash::Hash**: The user experience if this RFC is implemented ---------------------------------------------- @@ -109,7 +65,6 @@ Service teams face several challenges with the mandatory use of `smithy.framewor 1. **Backward compatibility**: Teams migrating existing APIs to Smithy cannot maintain their existing validation error format 2. **Published APIs**: Teams that have already published validation exception schemas to external consumers cannot change the response format without breaking clients 3. **Custom error handling**: Teams may need additional fields or different field names for their validation errors -4. **Legacy system integration**: Teams integrating with existing systems that expect specific validation error formats ### Solution: Custom validation exception traits @@ -129,7 +84,7 @@ structure CustomValidationException { #### 2. Specify the message field (required) -The custom validation exception **must** have exactly one String member marked with the `@validationMessage` trait to serve as the primary error message: +The custom validation exception **must** have **exactly one** String member marked with the `@validationMessage` trait to serve as the primary error message: ```smithy @validationException @@ -138,6 +93,8 @@ structure CustomValidationException { @validationMessage @required message: String + + // <... other fields ...> } ``` @@ -145,8 +102,8 @@ structure CustomValidationException { For the initial implementation, the custom validation exception structure **must** be default constructible. This means the shape either: -a. **must not** contain any constrained shapes that the framework cannot construct; or -b. any constrained shapes **must** have default values specified + 1. **must not** contain any constrained shapes that the framework cannot construct; or + 2. any constrained shapes **must** have default values specified ```smithy @validationException @@ -171,15 +128,13 @@ enum ErrorKind { #### 4. Optional field list support -Optionally, the custom validation exception **may** include a field marked with `@validationFieldList` to provide detailed information about which fields failed validation. This field can be one of: +Optionally, the custom validation exception **may** include a field marked with `@validationFieldList` to provide detailed information about which fields failed validation. This **must** be a list shape where the member is a structure shape with detailed field information: -a. A String shape (for simple field name listing) -b. A List shape where the member is a String shape (for multiple field names) -c. A List shape where the member is a structure shape with detailed field information - -For option (c), the structure shape: -- **must** have a String member marked with `@validationFieldName` -- **may** have a String member marked with `@validationFieldMessage` +* **must** have a String member marked with `@validationFieldName` +* **may** have a String member marked with `@validationFieldMessage` +* Regarding additional fields: + * The structure may have no additional fields beyond those specified above, or + * If additional fields are present, each must be default constructible ```smithy @validationException @@ -213,7 +168,7 @@ structure ValidationField { #### 5. Using custom validation exceptions in operations -Replace `smithy.framework#ValidationException` with your custom validation exception in operation error lists: +Replace `smithy.framework#ValidationException` with the custom validation exception in operation error lists: ```smithy operation GetUser { @@ -226,7 +181,18 @@ operation GetUser { } ``` -#### 6. Future extensibility +#### 6. Using different validation exceptions in operations + +For the initial implementation, we're adopting a simplified approach: validation exceptions cannot be mixed within a service. This means: + +* All operations within a service must use the same custom validation exception type, or +* All operations must use the standard Smithy validation exception + +While implementing support for multiple validation exception types would not be technically difficult, we've chosen to defer this complexity for the time being. + +Future enhancement: If developers need to use shapes from imported models that use either custom or standard Smithy validation exceptions, we plan to add a customization flag that will allow mapping these imported exceptions to a service's preferred exception type. This will enable greater flexibility when working with mixed models while maintaining consistency within a given service. + +#### 7. Future extensibility In a future iteration, the default constructibility requirement (rule #3) **may** be relaxed by allowing developers to register a factory function on the service builder. This factory function would be called by the framework whenever it needs to instantiate the custom validation exception, providing access to: @@ -247,16 +213,6 @@ let service = MyService::builder() .build(); ``` -### Additional use cases - -This RFC addresses several additional scenarios beyond basic backward compatibility: - -1. **Multi-language service teams**: Teams with clients in multiple programming languages may need validation errors in a format that's easier to parse in specific languages -2. **UI-focused applications**: Frontend applications may require structured field-level errors for form validation display -3. **Monitoring and analytics**: Teams may need additional metadata in validation errors for monitoring, logging, or analytics purposes -4. **Compliance requirements**: Some domains may have regulatory requirements for specific error message formats -5. **Internationalization**: Teams may need to include locale information or error codes that map to localized messages - ### Backwards compatibility This feature is entirely opt-in and maintains full backward compatibility: @@ -312,12 +268,10 @@ structure validationFieldMessage {} ### 2. Validation logic **Location**: `codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/validators/` - Add validation to ensure custom validation exceptions are properly defined: ```kotlin -class CustomValidationExceptionValidator : Validator { - override fun validate(model: Model): List { +class CustomValidationExceptionValidator : Validator { override fun validate(model: Model): List { val events = mutableListOf() model.shapes(StructureShape::class.java) @@ -362,73 +316,118 @@ class CustomValidationExceptionValidator : Validator { } ``` -### 3. Code generation modifications +### 3. Generated Rust code changes -**Location**: `codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/` +#### 3.1 `ValidationExceptionField` is independent of `ValidationException` -Modify the constraint violation handling to detect and use custom validation exceptions: +Each constraint violation, like `@required` or `@pattern`, is represented in a custom type called `ValidationExceptionField`. This type is independent of what shape has been modelled in the Smithy model. Therefore, there will be no change in this. -```kotlin -class CustomValidationExceptionGenerator( - private val model: Model, - private val symbolProvider: SymbolProvider, - private val rustCrate: RustCrate -) { - fun generateCustomValidationExceptionSupport(serviceShape: ServiceShape) { - val customValidationExceptions = findCustomValidationExceptions(serviceShape) - - customValidationExceptions.forEach { (operation, customException) -> - generateValidationExceptionMapper(operation, customException) +```rust +pub struct ValidationExceptionField { + /// A JSONPointer expression to the structure member whose value failed to satisfy the modeled constraints. + pub path: ::std::string::String, + /// A detailed description of the validation failure. + pub message: ::std::string::String, +} +``` + +#### 3.2 ValidationExceptionField to CustomValidationException + +Each operation's input (For example [GetStorage](https://github.com/smithy-lang/smithy-rs/blob/main/codegen-core/common-test-models/pokemon.smithy#L43) operation in the example model) has an associated `ConstraintViolation` enum type: + +```rust +pub mod get_storage_input { + pub enum ConstraintViolation { + /// `user` was not provided but it is required when building `GetStorageInput`. + MissingUser, + /// `passcode` was not provided but it is required when building `GetStorageInput`. + MissingPasscode, + } +} +``` +Which implements a `as_validation_exception_field` function to return a `ValidationExceptionField`, which would remain unchanged. + +```rust + impl ConstraintViolation { + pub(crate) fn as_validation_exception_field( + self, + path: ::std::string::String, + ) -> crate::model::ValidationExceptionField { + match self { + ConstraintViolation::MissingUser => crate::model::ValidationExceptionField { + message: format!("Value at '{}/user' failed to satisfy constraint: Member must not be null", path), + path: path + "/user", + }, + ConstraintViolation::MissingPasscode => crate::model::ValidationExceptionField { + message: format!("Value at '{}/passcode' failed to satisfy constraint: Member must not be null", path), + path: path + "/passcode", + }, + } } } - - private fun generateValidationExceptionMapper( - operation: OperationShape, - customException: StructureShape - ) { - val operationName = symbolProvider.toSymbol(operation).name - val exceptionSymbol = symbolProvider.toSymbol(customException) - - rustCrate.withModule(RustModule.private("validation_mappers")) { - rust(""" - pub(crate) fn map_constraint_violations_to_${operationName.toSnakeCase()}( - violations: crate::constrained::ConstraintViolations - ) -> ${exceptionSymbol.rustType().render()} { - ${generateMappingLogic(customException, violations)} - } - """) +``` + +Consequently, each `ConstraintViolation` also defines a conversion into `RequestRejection`, which is an enum with a variant `ConstraintViolation`. This is where the smithy model independent `ValidationExceptionField` gets converted into the model defined `ValidationException`, and would need to be changed. + +```rust +impl ::std::convert::From + for ::aws_smithy_http_server::protocol::rest_json_1::rejection::RequestRejection + { + fn from(constraint_violation: ConstraintViolation) -> Self { + let first_validation_exception_field = + constraint_violation.as_validation_exception_field("".to_owned()); + + // ---- Generate code for instantiating the CustomValidationException instead of Smithy's + + let validation_exception = crate::error::ValidationException { + message: format!( + "1 validation error detected. {}", + &first_validation_exception_field.message + ), + field_list: Some(vec![first_validation_exception_field]), + }; + + // ---- + Self::ConstraintViolation( + crate::protocol_serde::shape_validation_exception::ser_validation_exception_error(&validation_exception) + .expect("validation exceptions should never fail to serialize; please file a bug report under https://github.com/smithy-lang/smithy-rs/issues") + ) } } -} ``` -### 4. Framework integration +#### Protocol serialization changes -**Location**: `codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/protocol/` +A protocol dependent function is generated in the `protocol_serde` module for Smithy's `ValidationException`. This function will need to change to ouptut the fields of the `CustomValidationException`: + +```rust +pub fn ser_validation_exception( + object: &mut ::aws_smithy_json::serialize::JsonObjectWriter, + // Change this to `CustomValidationException` + input: &crate::error::ValidationException, +) -> ::std::result::Result<(), ::aws_smithy_types::error::operation::SerializationError> { -Update protocol generators to use custom validation exceptions when available: + // Serialize all fields of the CustomValidationException + +} +``` + +### 4. Code generator changes + +**Location**: `software/amazon/smithy/rust/codegen/server/smithy/customizations/CustomValidationGeneratorDecorator.kt` + +Most of the implementation is going to be similar to ` software/amazon/smithy/rust/codegen/server/smithy/customizations/SmithyValidationExceptionDecorator.kt` ```kotlin -// In the appropriate protocol generator -private fun generateConstraintViolationHandling(operation: OperationShape): Writable { - val customValidationException = findCustomValidationException(operation) - - return if (customValidationException != null) { - writable { - rust(""" - match constraint_violations { - Ok(input) => input, - Err(violations) => { - let custom_exception = crate::validation_mappers::map_constraint_violations_to_${operation.id.name.toSnakeCase()}(violations); - return Err(${customValidationException.name}(custom_exception).into()); - } - } - """) - } - } else { - // Use standard ValidationException - generateStandardValidationExceptionHandling() - } +class CustomValidationExceptionDecorator : ServerCodegenDecorator { + override val name: String + get() = "CustomValidationExceptionDecorator" + override val order: Byte + get() = 69 + + override fun validationExceptionConversion( + codegenContext: ServerCodegenContext, + ): ValidationExceptionConversionGenerator = CustomValidationExceptionConversionGenerator(codegenContext) } ``` From bcd24152464fabf2e5d2331c50d1006b11a6afe6 Mon Sep 17 00:00:00 2001 From: Fahad Zubair Date: Mon, 18 Aug 2025 19:01:09 +0100 Subject: [PATCH 4/6] Change checklist --- design/src/rfcs/rfc0047_custom_validation.md | 239 ++++++++++++++----- 1 file changed, 173 insertions(+), 66 deletions(-) diff --git a/design/src/rfcs/rfc0047_custom_validation.md b/design/src/rfcs/rfc0047_custom_validation.md index 22de2720392..ff09c622581 100644 --- a/design/src/rfcs/rfc0047_custom_validation.md +++ b/design/src/rfcs/rfc0047_custom_validation.md @@ -316,24 +316,22 @@ class CustomValidationExceptionValidator : Validator { override fun validate(mod } ``` -### 3. Generated Rust code changes +### 3. Generated Rust Code Changes -#### 3.1 `ValidationExceptionField` is independent of `ValidationException` +#### 3.1 Structure of Validation Exceptions -Each constraint violation, like `@required` or `@pattern`, is represented in a custom type called `ValidationExceptionField`. This type is independent of what shape has been modelled in the Smithy model. Therefore, there will be no change in this. +The `ValidationExceptionField` structure will remain unchanged as it's independent of the modeled `ValidationException` shape: ```rust pub struct ValidationExceptionField { - /// A JSONPointer expression to the structure member whose value failed to satisfy the modeled constraints. pub path: ::std::string::String, - /// A detailed description of the validation failure. pub message: ::std::string::String, } ``` -#### 3.2 ValidationExceptionField to CustomValidationException +#### 3.2 From Constraint Violations to Custom Exceptions -Each operation's input (For example [GetStorage](https://github.com/smithy-lang/smithy-rs/blob/main/codegen-core/common-test-models/pokemon.smithy#L43) operation in the example model) has an associated `ConstraintViolation` enum type: +**Step 1**: Each operation input has an associated `ConstraintViolation` enum representing possible validation failures: ```rust pub mod get_storage_input { @@ -345,78 +343,88 @@ pub mod get_storage_input { } } ``` -Which implements a `as_validation_exception_field` function to return a `ValidationExceptionField`, which would remain unchanged. + +**Step 2**: Each `ConstraintViolation` can be converted to a `ValidationExceptionField`: ```rust - impl ConstraintViolation { - pub(crate) fn as_validation_exception_field( - self, - path: ::std::string::String, - ) -> crate::model::ValidationExceptionField { - match self { +impl ConstraintViolation { + pub(crate) fn as_validation_exception_field( + self, + path: ::std::string::String, + ) -> crate::model::ValidationExceptionField { + match self { ConstraintViolation::MissingUser => crate::model::ValidationExceptionField { - message: format!("Value at '{}/user' failed to satisfy constraint: Member must not be null", path), - path: path + "/user", - }, + message: format!("Value at '{}/user' failed to satisfy constraint: Member must not be null", path), + path: path + "/user", + }, ConstraintViolation::MissingPasscode => crate::model::ValidationExceptionField { - message: format!("Value at '{}/passcode' failed to satisfy constraint: Member must not be null", path), - path: path + "/passcode", - }, - } + message: format!("Value at '{}/passcode' failed to satisfy constraint: Member must not be null", path), + path: path + "/passcode", + }, } } +} ``` -Consequently, each `ConstraintViolation` also defines a conversion into `RequestRejection`, which is an enum with a variant `ConstraintViolation`. This is where the smithy model independent `ValidationExceptionField` gets converted into the model defined `ValidationException`, and would need to be changed. +**Step 3**: The `From` implementation for `RequestRejection` needs modification to use the custom exception: ```rust -impl ::std::convert::From - for ::aws_smithy_http_server::protocol::rest_json_1::rejection::RequestRejection - { - fn from(constraint_violation: ConstraintViolation) -> Self { - let first_validation_exception_field = - constraint_violation.as_validation_exception_field("".to_owned()); - - // ---- Generate code for instantiating the CustomValidationException instead of Smithy's - - let validation_exception = crate::error::ValidationException { - message: format!( - "1 validation error detected. {}", - &first_validation_exception_field.message - ), - field_list: Some(vec![first_validation_exception_field]), - }; - - // ---- - Self::ConstraintViolation( - crate::protocol_serde::shape_validation_exception::ser_validation_exception_error(&validation_exception) - .expect("validation exceptions should never fail to serialize; please file a bug report under https://github.com/smithy-lang/smithy-rs/issues") - ) - } +impl From for RequestRejection { + fn from(constraint_violation: ConstraintViolation) -> Self { + // Convert the constraint violation to a ValidationExceptionField + let field = constraint_violation.as_validation_exception_field("".to_owned()); + + // CHANGE: Create CustomValidationException instead of ValidationException + let custom_exception = crate::error::CustomValidationException::builder() + .message(format!("1 validation error detected. {}", &field.message)) + .field_list(Some(vec![field])) + .build(); + + Self::ConstraintViolation( + // CHANGE: Call serializer for CustomValidationException + crate::protocol_serde::shape_custom_validation_exception::ser_custom_validation_exception_error(&custom_exception) + .expect("serialization should not fail") + ) } +} ``` -#### Protocol serialization changes +#### 3.3 Serialization Function -A protocol dependent function is generated in the `protocol_serde` module for Smithy's `ValidationException`. This function will need to change to ouptut the fields of the `CustomValidationException`: +A new serialization function must be implemented for the custom exception: ```rust -pub fn ser_validation_exception( - object: &mut ::aws_smithy_json::serialize::JsonObjectWriter, - // Change this to `CustomValidationException` - input: &crate::error::ValidationException, -) -> ::std::result::Result<(), ::aws_smithy_types::error::operation::SerializationError> { - - // Serialize all fields of the CustomValidationException - -} +// CHANGE: New serialization function for CustomValidationException +pub fn ser_custom_validation_exception( + object: &mut JsonObjectWriter, + input: &crate::error::CustomValidationException, +) -> Result<(), SerializationError> { + // Serialize standard fields + object.key("message").string(&input.message); + + // Serialize validation field list + if let Some(fields) = &input.field_list { + let mut array = object.key("fieldList").array(); + for item in fields { + let mut obj = array.object(); + crate::protocol_serde::shape_validation_exception_field::ser_validation_exception_field(&mut obj, item)?; + obj.finish(); + } + array.finish(); + } + + // Serialize any custom fields + // [serialization code for custom fields] + + Ok(()) +} ``` -### 4. Code generator changes +### 4. Code Generator Changes **Location**: `software/amazon/smithy/rust/codegen/server/smithy/customizations/CustomValidationGeneratorDecorator.kt` -Most of the implementation is going to be similar to ` software/amazon/smithy/rust/codegen/server/smithy/customizations/SmithyValidationExceptionDecorator.kt` +To support custom validation exceptions, we need to create a new decorator that follows a similar pattern to the existing `SmithyValidationExceptionDecorator`. The key changes are: ```kotlin class CustomValidationExceptionDecorator : ServerCodegenDecorator { @@ -424,13 +432,112 @@ class CustomValidationExceptionDecorator : ServerCodegenDecorator { get() = "CustomValidationExceptionDecorator" override val order: Byte get() = 69 - override fun validationExceptionConversion( codegenContext: ServerCodegenContext, ): ValidationExceptionConversionGenerator = CustomValidationExceptionConversionGenerator(codegenContext) } ``` +The existing validation field generation logic should be refactored into a common class: + +```kotlin +// New shared utility class +class ValidationExceptionFieldGenerator(private val codegenContext: ServerCodegenContext) { + // Common code for generating ValidationExceptionField structures and conversion methods + fun generateValidationExceptionField(): Writable { + // Implementation moved from SmithyValidationExceptionDecorator + } +} +``` + +#### Define a Builder for the `CustomValidationException` + +Unlike Smithy's standard `ValidationException` which doesn't have a builder, we should generate a builder for the `CustomValidationException` to simplify its construction and allow for default values: + +```kotlin +fun generateCustomValidationExceptionBuilder(): Writable { + return writer { + write(""" + impl ${codegenContext.customExceptionName} { + /// Create a new builder for the custom validation exception + pub fn builder() -> ${codegenContext.customExceptionName}Builder { + ${codegenContext.customExceptionName}Builder::default() + } + } + + /// Builder for ${codegenContext.customExceptionName} + #[derive(Default)] + pub struct ${codegenContext.customExceptionName}Builder { + message: Option, + field_list: Option>, + // Add additional fields from the custom exception model + ${renderAdditionalBuilderFields()} + } + + impl ${codegenContext.customExceptionName}Builder { + /// Set the error message + pub fn message(mut self, message: impl Into) -> Self { + self.message = Some(message.into()); + self + } + + /// Set the list of validation exception fields + pub fn field_list(mut self, field_list: Vec) -> Self { + self.field_list = Some(field_list); + self + } + + ${renderAdditionalBuilderMethods()} + + /// Build the custom validation exception + pub fn build(self) -> Result<${codegenContext.customExceptionName}, String> { + let message = self.message.ok_or("message is required")?; + + Ok(${codegenContext.customExceptionName} { + message, + field_list: self.field_list, + ${renderAdditionalBuildFields()} + }) + } + } + """.trimIndent()) + } +} +``` + +Then, the custom validation exception generator would implement the conversion from constraint violations to the custom exception: + +```kotlin +class CustomValidationExceptionConversionGenerator(private val codegenContext: ServerCodegenContext) : + ValidationExceptionConversionGenerator { + + override fun renderImplFromConstraintViolationForRequestRejection(protocol: ServerProtocol): Writable { + return writer { + write(""" + impl #{From} for #{RequestRejection} { + fn from(constraint_violation: ConstraintViolation) -> Self { + let first_validation_exception_field = + constraint_violation.as_validation_exception_field("".to_owned()); + + // Create custom validation exception using the builder + let custom_exception = crate::error::${codegenContext.customExceptionName}::builder() + .message(format!("1 validation error detected. {}", &first_validation_exception_field.message)) + .field_list(vec![first_validation_exception_field]) + .build() + .expect("Custom validation exception should be valid"); + + Self::ConstraintViolation( + crate::protocol_serde::shape_${codegenContext.customExceptionName.decapitalize()}::ser_${codegenContext.customExceptionName.decapitalize()}_error(&custom_exception) + .expect("validation exceptions should never fail to serialize") + ) + } + } + """.trimIndent()) + } + } +} +``` + ### 5. Testing strategy Comprehensive testing is required to ensure the feature works correctly: @@ -441,18 +548,18 @@ Comprehensive testing is required to ensure the feature works correctly: 4. **Backward compatibility tests**: Ensure existing services continue to work unchanged 5. **Error message tests**: Verify custom validation exceptions contain expected field information -Changes checklist +Changes Checklist ----------------- - - [ ] Create `validationException`, `validationMessage`, `validationFieldList`, `validationFieldName`, and `validationFieldMessage` traits in `codegen-server-traits` - [ ] Implement `CustomValidationExceptionValidator` to validate proper usage of custom validation exception traits -- [ ] Create `CustomValidationExceptionGenerator` to generate mapping logic from constraint violations to custom exceptions -- [ ] Modify protocol generators to detect and use custom validation exceptions instead of standard `ValidationException` -- [ ] Update constraint violation handling in server request processing to use custom validation exception mappers -- [ ] Generate default constructors for custom validation exception shapes +- [ ] Create shared `ValidationExceptionFieldGenerator` for common validation field generation logic +- [ ] Implement `CustomValidationExceptionDecorator` and `CustomValidationExceptionConversionGenerator` to generate custom exception mapping logic +- [ ] Add builder pattern generation for custom validation exception shapes via `CustomValidationExceptionBuilderGenerator` +- [ ] Update `From` implementations to create custom exceptions instead of `ValidationException` +- [ ] Implement serialization functions for custom validation exception shapes - [ ] Add comprehensive unit tests for trait validation logic - [ ] Add integration tests for end-to-end custom validation exception handling - [ ] Create documentation explaining custom validation exception usage and migration strategies - [ ] Add examples showing various custom validation exception patterns - [ ] Update existing constraint violation documentation to mention custom validation exceptions -- [ ] Ensure backward compatibility with existing services using standard `ValidationException` +- [ ] Ensure backward compatibility with existing services using standard `ValidationException` \ No newline at end of file From a56bd47ff6eda6110d44a545580a29757c404e8d Mon Sep 17 00:00:00 2001 From: Fahad Zubair Date: Mon, 18 Aug 2025 19:03:57 +0100 Subject: [PATCH 5/6] Fix typos --- design/src/rfcs/rfc0047_custom_validation.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/design/src/rfcs/rfc0047_custom_validation.md b/design/src/rfcs/rfc0047_custom_validation.md index ff09c622581..09269270cd9 100644 --- a/design/src/rfcs/rfc0047_custom_validation.md +++ b/design/src/rfcs/rfc0047_custom_validation.md @@ -522,7 +522,7 @@ class CustomValidationExceptionConversionGenerator(private val codegenContext: S // Create custom validation exception using the builder let custom_exception = crate::error::${codegenContext.customExceptionName}::builder() .message(format!("1 validation error detected. {}", &first_validation_exception_field.message)) - .field_list(vec![first_validation_exception_field]) + .field_list(Some(vec![first_validation_exception_field])) .build() .expect("Custom validation exception should be valid"); From 50c0f8ee84b0aaf6f99c77c041f7c00cb4be9db8 Mon Sep 17 00:00:00 2001 From: Fahad Zubair Date: Mon, 18 Aug 2025 19:08:33 +0100 Subject: [PATCH 6/6] Add overview and summary --- design/src/SUMMARY.md | 1 + design/src/rfcs/overview.md | 1 + 2 files changed, 2 insertions(+) diff --git a/design/src/SUMMARY.md b/design/src/SUMMARY.md index c374a6bee07..3dda07c2e8e 100644 --- a/design/src/SUMMARY.md +++ b/design/src/SUMMARY.md @@ -68,5 +68,6 @@ - [RFC-0043: Identity Cache Partitions](./rfcs/rfc0043_identity_cache_partitions.md) - [RFC-0044: Environment-defined service configuration](./rfcs/rfc0044_env_defined_service_config.md) - [RFC-0045: Configurable Serde](./rfcs/rfc0045_configurable_serde.md) + - [RFC-0047: Custom ValidationException](./rfcs/rfc0047_custom_validation.md) - [Contributing](./contributing/overview.md) - [Writing and debugging a low-level feature that relies on HTTP](./contributing/writing_and_debugging_a_low-level_feature_that_relies_on_HTTP.md) diff --git a/design/src/rfcs/overview.md b/design/src/rfcs/overview.md index 36655db077b..e6b14d650f9 100644 --- a/design/src/rfcs/overview.md +++ b/design/src/rfcs/overview.md @@ -54,4 +54,5 @@ - [RFC-0042: File-per-change changelog](./rfc0042_file_per_change_changelog.md) - [RFC-0043: Identity Cache Partitions](./rfc0043_identity_cache_partitions.md) - [RFC-0045: Configurable Serde](./rfc0045_configurable_serde.md) +- [RFC-0047: Custom ValidationException](./rfc0047_custom_validation.md)