Give every validation failure one coded problem shape (string matching) - #8242
Draft
Hinton wants to merge 1 commit into
Draft
Give every validation failure one coded problem shape (string matching)#8242Hinton wants to merge 1 commit into
Hinton wants to merge 1 commit into
Conversation
A request that failed its DataAnnotations answered with the ErrorResponseModel envelope while a request its handler rejected answered with an RFC 7807 problem document. Same endpoint, same field, two bodies and two key conventions — a client needed both parsers, and only one of them carried a code it could switch on. One document now serves both, keyed by the property, carrying a code that names what went wrong and the substitutions a client needs to say so in its own language. Codes come from ValidationCodes and parameter keys from ValidationParameters, shared lists both sides draw on, because a client looking a substitution up by name is broken by a second spelling as surely as by a second code. Parameters carry the limit that was breached and nothing derived from the value that breached it. DataAnnotations records a message and discards the constraint behind it, but the sentence it leaves still contains the limit: the 200 in StringLength(200) is in "must be a string with a maximum length of 200". ValidationMessageCodes recognises each attribute's wording and lifts the value back out. That makes the wording the contract, which is the trade this takes knowingly. Reading the constraint off the model instead would mean reflection, which is not trim-safe, or a source generator, which is more machinery than the surface currently justifies. Recognising a sentence is neither: it needs no registration and no RequiresUnreferencedCode, so HttpExtensions builds with IsAotCompatible and is clean, publishable from a trimmed or ahead-of-time minimal API as it stands. What it costs is written down. A reworded message loses its code and keeps its detail, reported as invalid rather than as something wrong; ValidationMessageCodesTests asserts every pattern against the message its attribute actually produces, so a reword fails the build on the next SDK bump instead of in production. An explicit ErrorMessage matches nothing and reports the same way. A property renamed by JsonPropertyName is keyed by its camel-cased CLR name, as the previous envelope also did, and a bound lifted out of prose is the framework's rendering of it rather than the declared literal. The document is behind CodedValidationProblems. It replaces a body clients are already parsing, so each surface opts in on its own schedule and only the internal API does today; the public API keeps its published shape, which is versioned on its own terms. With the flag off, nothing answers differently.
| await result.ExecuteAsync(context); | ||
|
|
||
| stream.Position = 0; | ||
| return Indent(await new StreamReader(stream).ReadToEndAsync()); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
🎟️ Tracking
No ticket — foundation extracted from the PAM error-codes work so it can land on
mainindependently of it.📔 Objective
A request that failed its DataAnnotations answered with the
ErrorResponseModelenvelope while a request its handler rejected answered with an RFC 7807 problem document. Same endpoint, same field, two bodies and two key conventions — a client needed both parsers, and only one of them carried a code it could switch on.One document now serves both:
{ "type": "validation_error", "title": "One or more validation errors occurred.", "status": 400, "errors": { "reason": [{ "type": "required", "detail": "The Reason field is required." }], "name": [{ "type": "too_long", "detail": "The field Name must be a string with a maximum length of 200.", "parameters": { "max": 200 } }] } }One vocabulary. Codes come from
ValidationCodesand parameter keys fromValidationParameters. A client looking a substitution up by name is broken by a second spelling as surely as by a second code. A code names what went wrong and never the field it is keyed under —required, notname_required. Parameters carry the limit that was breached and nothing derived from the value that breached it.How the code is recovered
DataAnnotations records a message and discards the constraint behind it — but the sentence it leaves still contains the limit. The
200in[StringLength(200)]is right there in "must be a string with a maximum length of 200."ValidationMessageCodesis ~12[GeneratedRegex]patterns that recognise each attribute's wording and lift the value back out.Nothing validates anything; the framework still decides. Nothing reflects, either — so there is no generator, no registration step, and not one
[RequiresUnreferencedCode].HttpExtensionsbuilds withIsAotCompatibleand is analyzer-clean as it stands, publishable from a trimmed or AOT minimal API.What this costs, and why it is written down
The wording is the contract. That is the trade, taken knowingly:
invalidErrorMessage(~5% of the repo's attributes)invalid[JsonPropertyName]rename"2020-01-01 00:00:00", not the declared literalNothing is ever dropped or mis-coded — the failure always reaches the client with its message. And
ValidationMessageCodesTestsasserts every pattern against the message its attribute actually produces, by callingFormatErrorMessageon the real attribute, so a framework reword fails the build on the next SDK bump rather than degrading silently in production.All four rows are fixable only by reading the model's attributes rather than its messages — with reflection, which is not trim-safe, or a generator, which is #8237.
Notes for review
FeatureFlagKeys.CodedValidationProblems; off means nothing changes. Each surface opts in separately —TryCodedProblemoffers it and only the internal Api takes it. The public API keeps its published shape.src/HttpExtensions/README.mddocuments the design and the four costs above.ValidationProblemDocumentTestsasserts the complete serialized body against a literal, so the wire format has a spec that fails on any shape change.ValidationRoundTripTestsdrives a real MVC pipeline end to end and asserts on codes.Known gaps, deliberately not in scope
ExceptionHandlerFilterAttributestill answers withErrorResponseModel, sothrow new BadRequestException(modelState)bypasses this. Same in both PRs; the natural follow-up..Produces<BitwardenValidationProblemDetails>(400)is not declared on endpoints, so OpenAPI does not yet describe the coded shape.📸 Screenshots
N/A