Skip to content

OpenAPI Schema and Examples Source Generation#44

Merged
feO2x merged 17 commits into
mainfrom
43-openapi-source-generation
May 22, 2026
Merged

OpenAPI Schema and Examples Source Generation#44
feO2x merged 17 commits into
mainfrom
43-openapi-source-generation

Conversation

@feO2x
Copy link
Copy Markdown
Owner

@feO2x feO2x commented May 21, 2026

Closes #43

Summary

The original direction stayed intact: validation OpenAPI metadata is generated from synchronous validators, emitted through a static contract, and consumed by Minimal API endpoint metadata without reflection. The implementation added two follow-up capabilities that were not fully covered by 0043-0: explicit documentation hints for opaque validation paths, and representative per-error example messages.

Most boundaries from the original plan still hold. The generator remains Minimal API-focused, supports direct Validator<T> and Validator<TSource, TValidated> inheritance only, skips nested control-flow checks with diagnostics, treats Must(...), Custom(...), and ErrorOverrides as opaque unless documented explicitly, and keeps generated code NativeAOT-safe.

Runtime API Deviations

The generated contract target is the public static-abstract interface IPortableValidationOpenApiContract in Light.PortableResults.Validation.OpenApi. Generated validators implement this interface and expose ConfigurePortableValidationOpenApi(PortableValidationProblemOpenApiBuilder builder), matching the original reflection-free design.

The Minimal API bridge is implemented as ProducesPortableValidationProblemFor<TValidator>(...) in Light.PortableResults.Validation.OpenApi. It delegates to the existing ASP.NET Core OpenAPI builder, first applying the generated validator contract and then invoking the caller's configure callback. This preserves the intended ordering: generated metadata is the baseline, endpoint-local manual configuration can override or extend it.

PortableOpenApiErrorExampleEntry and WithErrorExample(...) were extended beyond the original plan to include a nullable per-error message before metadata. The OpenAPI transformer now uses that message for both rich validation problem examples and ASP.NET Core-compatible validation problem examples, falling back to "Validation failed." when no message is supplied.

Hint Model Deviations

The first plan treated explicit emitted-error hints as an escape hatch but did not fully specify their public API. The implementation made this a first-class feature:

  • PortableValidationOpenApiErrorHintAttribute documents a known error code and can optionally point to a metadata type.
  • PortableValidationOpenApiErrorMetadataPropertyAttribute documents inline metadata schema properties when a dedicated metadata type would be unnecessary or awkward.
  • PortableValidationOpenApiExampleHintAttribute documents response-example entries for opaque paths and can include Target and Message.
  • PortableValidationOpenApiExampleMetadataAttribute supplies compile-time constant metadata values for matching example hints.

Hints can be placed on the validator class or directly on PerformValidation. They compose with inferred rules, are deduplicated when compatible, and produce diagnostics when schema contracts conflict. Example-only hints also document the code as a code-only schema entry, avoiding a redundant error hint for common opaque paths.

AllowUnknownErrorCodes is available on GeneratePortableValidationOpenApiAttribute as an explicit opt-in. Hints do not imply it. This is a sharper separation than the original plan text: hints document known contracts, while AllowUnknownErrorCodes keeps the response schema non-exhaustive for additional codes that are not enumerable at build time.

Message Deviations

The implementation added ValidationRuleMessageAttribute in Light.PortableResults.Validation.Definitions. Built-in validation check methods now carry compile-time message templates where the default framework message can be represented statically.

The generator substitutes {displayName} and metadata placeholders such as {minLength}, {maxLength}, {lowerBoundary}, {upperBoundary}, and {comparativeValue} when all required values are known at compile time. The display name is resolved from an explicit constant displayName argument to ValidationContext.Check(...), then from the inferred JSON-style target, and otherwise omitted.

If a template is valid but a value is not statically known, the generator still emits the schema and example entry but leaves the message as null, letting the transformer use the fallback message. Invalid templates produce warning diagnostics:

  • LPRSG0013 for unknown placeholders.
  • LPRSG0014 for malformed brace sequences or unsupported placeholder syntax.

This is a deliberate documentation feature only. Generated messages are representative defaults and are not intended to exactly model runtime customization, localization, culture-specific formatting, display-name customization, or ErrorOverrides.Message.

Analysis and Generation Deviations

The source generator is implemented as an IIncrementalGenerator using ForAttributeWithMetadataName(...), as planned. The thin generator adapter calls the public ValidatorOpenApiAnalyzer.Analyze(...) method, which returns a ValidatorOpenApiAnalysis containing diagnostics, hint name, and source. The implementation therefore made the analysis directly testable without a full generator-driver flow.

The generator does not reference runtime projects as normal assembly references. It resolves contracts by metadata name through KnownTypeNames, while generated source depends on the runtime packages in the consuming application.

Generated source uses a deterministic controlled using block and emits builder calls rather than runtime reflection. It now emits:

  • grouped WithErrorCodes(...) calls for registered no-metadata or globally registered contracts;
  • typed helper calls such as WithInRangeError<T>() for comparison and range contracts;
  • inline WithErrorMetadata(...) calls for annotated custom rules and inline hint metadata;
  • WithErrorExample(...) calls containing code, target, optional representative message, and constant metadata values where available;
  • AllowUnknownErrorCodes() only when explicitly requested.

The implementation also chose direct in-memory Roslyn generator tests instead of introducing snapshot infrastructure such as Verify.SourceGenerators. This still covers emitted source, diagnostics, and generated OpenAPI document behavior while keeping the test stack smaller.

Validation Rule Annotation Deviations

The core validation layer gained source-generator-facing annotations in Light.PortableResults.Validation.Definitions:

  • ValidationRuleAttribute
  • ValidationRuleMetadataAttribute
  • ValidationRuleMessageAttribute
  • ValidationErrorContractAttribute
  • ValidationErrorMetadataContractAttribute

These remain OpenAPI-agnostic and use source-generator-friendly attribute shapes only. Built-in checks were annotated rather than hard-coded into the generator. The comparative range rename from IsInBetween / IsNotInBetween to IsInRange / IsNotInRange was applied, aligning method names with ValidationErrorCodes.InRange and ValidationErrorCodes.NotInRange.

One practical addition beyond the first plan is inline metadata schema generation for user-defined rules and hints. When an annotated rule references a validation error contract, the generator can emit an endpoint-local schema with PortableOpenApiSchemaTypeMapper.Map<T>() instead of requiring a pre-registered metadata type for every custom case.

Documentation, Sample, and Tests

The NativeAotMovieRating sample now uses [GeneratePortableValidationOpenApi] on NewMovieRatingValidator and ProducesPortableValidationProblemFor<NewMovieRatingValidator>(...) on the endpoint. Manual validation error-code configuration was removed from the sample endpoint, with endpoint-local format configuration kept in the callback.

The README now documents the generator opt-in model, supported validator shapes, top-level-only analysis boundary, explicit hints, example metadata, representative messages, and the distinction between hints and AllowUnknownErrorCodes().

Tests were added across the source-generation and OpenAPI projects for generator output, diagnostics, custom annotated rules, hints, example metadata, messages, builder behavior, document transformation, and generated OpenAPI integration. The package layout also bundles the generator into Light.PortableResults.Validation.OpenApi as an analyzer asset under analyzers/dotnet/cs, matching the original distribution intent.

Remaining Boundaries

The following limitations remain intentional after the deviations:

  • MVC integration is still outside this implementation.
  • Async validators remain unsupported.
  • Indirect/custom validator base classes remain unsupported.
  • Nested checks inside control flow remain skipped rather than analyzed.
  • Complex target inference, child validators, automatic source-null errors before PerformValidation, and runtime-customized validation messages remain outside the generator's static analysis scope.
  • Opaque delegate or imperative validation remains documentable only through explicit hints or endpoint-level manual configuration.

feO2x and others added 14 commits May 2, 2026 12:25
Signed-off-by: Kenny Pflug <kenny.pflug@live.de>
Tighten scope to Minimal APIs and sync validators only, promote
response-level examples and light target inference to first-class
features, add bridge API extensions for example emission, and clarify
diagnostics, NativeAOT constraints, and incremental-pipeline guidance.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: Kenny Pflug <kenny.pflug@live.de>
Signed-off-by: Kenny Pflug <kenny.pflug@live.de>
Signed-off-by: Kenny Pflug <kenny.pflug@live.de>
…nalyzer

Signed-off-by: Kenny Pflug <kenny.pflug@live.de>
Signed-off-by: Kenny Pflug <kenny.pflug@live.de>
Signed-off-by: Kenny Pflug <kenny.pflug@live.de>
Signed-off-by: Kenny Pflug <kenny.pflug@live.de>
Signed-off-by: Kenny Pflug <kenny.pflug@live.de>
Signed-off-by: Kenny Pflug <kenny.pflug@live.de>
Signed-off-by: Kenny Pflug <kenny.pflug@live.de>
Signed-off-by: Kenny Pflug <kenny.pflug@live.de>
Signed-off-by: Kenny Pflug <kenny.pflug@live.de>
@feO2x feO2x self-assigned this May 21, 2026
@feO2x feO2x added the enhancement New feature or request label May 21, 2026
@feO2x feO2x requested a review from Copilot May 21, 2026 19:28
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds a Minimal API–focused OpenAPI source generation pipeline for Light.PortableResults.Validation, allowing synchronous validators to emit reflection-free, NativeAOT-safe endpoint OpenAPI metadata (schemas + response examples). It also introduces a hint/annotation model to document otherwise opaque validation flows and enriches response examples with representative per-error messages.

Changes:

  • Added a new incremental source generator (Light.PortableResults.Validation.OpenApi.SourceGeneration) and bundled it into Light.PortableResults.Validation.OpenApi as an analyzer asset.
  • Introduced generator-facing validation rule/contract/message attributes plus OpenAPI-facing opt-in + hint attributes, and added a Minimal API bridge (ProducesPortableValidationProblemFor<TValidator>).
  • Extended OpenAPI response-example plumbing to support per-error example messages; updated built-in range API naming (IsInRange / IsNotInRange) and propagated it across code/tests/docs/samples.

Reviewed changes

Copilot reviewed 62 out of 62 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
tests/package-consumer/validate-validation-openapi-source-generation.sh Packs local NuGet feed and validates generator works in a package consumer build.
tests/Light.PortableResults.Validation.Tests/ValidationErrorDefinitionTests.cs Updates tests for IsInRange/IsNotInRange rename.
tests/Light.PortableResults.Validation.Tests/CheckShortCircuitCoverageTests.cs Updates short-circuit coverage tests for range rename.
tests/Light.PortableResults.Validation.Tests/CheckOverloadCoverageTests.cs Updates overload tests for range rename.
tests/Light.PortableResults.Validation.Tests/BuiltInRuleFamilyWorkflowTests.cs Updates workflow tests for range rename.
tests/Light.PortableResults.Validation.OpenApi.Tests/Light.PortableResults.Validation.OpenApi.Tests.csproj Adds generator project as analyzer reference for integration tests.
tests/Light.PortableResults.Validation.OpenApi.Tests/GeneratedValidationOpenApiIntegrationTests.cs New end-to-end test verifying generated schemas + examples appear on OpenAPI output.
tests/Light.PortableResults.Validation.OpenApi.SourceGeneration.Tests/xunit.runner.json Enables parallel test execution for generator test project.
tests/Light.PortableResults.Validation.OpenApi.SourceGeneration.Tests/PortableValidationOpenApiGeneratorTests.cs New in-memory Roslyn tests for generator output, hints, messages, and diagnostics.
tests/Light.PortableResults.Validation.OpenApi.SourceGeneration.Tests/packages.lock.json Adds locked dependencies for new generator test project.
tests/Light.PortableResults.Validation.OpenApi.SourceGeneration.Tests/Light.PortableResults.Validation.OpenApi.SourceGeneration.Tests.csproj New test project for generator analysis/output validation.
tests/Light.PortableResults.AspNetCore.OpenApi.Tests/PortableResultsOpenApiDocumentTransformerTests.cs Adds tests asserting message-aware example behavior for both validation formats.
tests/Light.PortableResults.AspNetCore.OpenApi.Tests/PortableOpenApiResponseBuilderTests.cs Adds tests for message-aware example equality and builder accumulation.
src/Light.PortableResults.Validation/Messaging/ValidationErrorTemplates.cs Renames range templates to IsInRange / NotInRange.
src/Light.PortableResults.Validation/Definitions/ValidationRuleAttributes.cs Adds generator-facing rule/metadata/message + error-contract attributes.
src/Light.PortableResults.Validation/Definitions/BuiltInValidationErrorDefinitions.Comparable.cs Renames range definition APIs/types to InRange/NotInRange.
src/Light.PortableResults.Validation/Checks.Strings.cs Annotates built-in string checks with rule/message/metadata attributes.
src/Light.PortableResults.Validation/Checks.Null.cs Annotates null checks with rule/message attributes.
src/Light.PortableResults.Validation/Checks.Equality.cs Annotates equality checks with typed-comparison rule/message/metadata attributes.
src/Light.PortableResults.Validation/Checks.Enums.cs Annotates enum checks; adjusts nullability in Enum.IsDefined calls.
src/Light.PortableResults.Validation/Checks.Empty.cs Annotates empty/not-empty checks with rule/message attributes.
src/Light.PortableResults.Validation/Checks.Decimals.cs Annotates precision/scale checks with rule/metadata attributes.
src/Light.PortableResults.Validation/Checks.Count.cs Annotates count checks with rule/message/metadata attributes.
src/Light.PortableResults.Validation/Checks.Comparable.cs Renames IsInBetweenIsInRange, adds rule/message/metadata annotations for comparisons/ranges.
src/Light.PortableResults.Validation.OpenApi/PortableValidationOpenApiRouteHandlerBuilderExtensions.cs Adds ProducesPortableValidationProblemFor<TValidator> Minimal API bridge.
src/Light.PortableResults.Validation.OpenApi/PortableValidationOpenApiExampleMetadataAttribute.cs Adds attribute for constant example metadata values.
src/Light.PortableResults.Validation.OpenApi/PortableValidationOpenApiExampleHintAttribute.cs Adds attribute for example hints (code/target/message).
src/Light.PortableResults.Validation.OpenApi/PortableValidationOpenApiErrorMetadataPropertyAttribute.cs Adds attribute for inline metadata schema properties.
src/Light.PortableResults.Validation.OpenApi/PortableValidationOpenApiErrorHintAttribute.cs Adds attribute for explicit error code + optional metadata type hints.
src/Light.PortableResults.Validation.OpenApi/Light.PortableResults.Validation.OpenApi.csproj Bundles generator as analyzer asset into OpenAPI package.
src/Light.PortableResults.Validation.OpenApi/IPortableValidationOpenApiContract.cs Adds static-abstract contract interface for generated validator metadata.
src/Light.PortableResults.Validation.OpenApi/GeneratePortableValidationOpenApiAttribute.cs Adds opt-in attribute with AllowUnknownErrorCodes.
src/Light.PortableResults.Validation.OpenApi/BuiltInValidationErrorBuilderExtensions.cs Extends typed helper APIs to optionally add example entries.
src/Light.PortableResults.Validation.OpenApi.SourceGeneration/ValidatorOpenApiModels.cs Adds analysis DTOs and equality/comparer helpers for generator pipeline.
src/Light.PortableResults.Validation.OpenApi.SourceGeneration/ValidatorOpenApiEmitter.cs Emits deterministic generated partial validator contract source.
src/Light.PortableResults.Validation.OpenApi.SourceGeneration/PortableValidationOpenApiGenerator.cs Implements incremental generator adapter with ForAttributeWithMetadataName.
src/Light.PortableResults.Validation.OpenApi.SourceGeneration/packages.lock.json Adds locked dependencies for generator project.
src/Light.PortableResults.Validation.OpenApi.SourceGeneration/Light.PortableResults.Validation.OpenApi.SourceGeneration.csproj New netstandard2.0 generator project definition.
src/Light.PortableResults.Validation.OpenApi.SourceGeneration/KnownTypeNames.cs Centralizes metadata-name constants for generator symbol resolution.
src/Light.PortableResults.Validation.OpenApi.SourceGeneration/DiagnosticDescriptors.cs Adds LPRSG diagnostics for unsupported shapes, hints, and message templates.
src/Light.PortableResults.Validation.OpenApi.SourceGeneration/CodeWriter.cs Adds simple indentation-aware code writer for emitted source.
src/Light.PortableResults.AspNetCore.OpenApi/Schemas/PortableResultsOpenApiSchemas.cs Adds example value for ErrorCategory schema.
src/Light.PortableResults.AspNetCore.OpenApi/PortableValidationProblemOpenApiBuilder.cs Adds message-aware WithErrorExample builder API.
src/Light.PortableResults.AspNetCore.OpenApi/PortableProblemOpenApiBuilder.cs Adds message-aware WithErrorExample builder API.
src/Light.PortableResults.AspNetCore.OpenApi/PortableOpenApiErrorResponseAttributeBase.cs Adds ErrorExamples storage to route metadata attribute base.
src/Light.PortableResults.AspNetCore.OpenApi/PortableOpenApiErrorExampleEntry.cs Introduces example-entry model with message + metadata and equality/hash.
src/Light.PortableResults.AspNetCore.OpenApi/PortableOpenApiBuilderUtilities.cs Adds append helper for accumulating example entries.
src/Light.PortableResults.AspNetCore.OpenApi/Generation/PortableResultsOpenApiDocumentTransformer.cs Composes configured error examples into OpenAPI response examples (rich + ASP.NET compatible).
samples/NativeAotMovieRating/NewMovieRating/NewMovieRatingValidator.cs Opts validator into generator and updates range call.
samples/NativeAotMovieRating/NewMovieRating/NewMovieRatingEndpoint.cs Switches endpoint to ProducesPortableValidationProblemFor<...> using generated contract.
samples/NativeAotMovieRating/NativeAotMovieRating.csproj Adds generator project as analyzer reference for sample.
samples/NativeAotMovieRating/GetMovies/GetMoviesEndpoint.cs Updates range call to IsInRange.
README.md Documents the generator, hints, example metadata/messages, and limitations; updates range naming.
Light.PortableResults.slnx Adds generator project and new test project; adds AI plan docs.
Directory.Packages.props Centrally pins Roslyn analyzer/generator package versions.
benchmarks/Benchmarks/packages.lock.json Updates lock file to reflect central Roslyn package versions and package version bumps.
benchmarks/Benchmarks/FlatDtoValidationBenchmarks.cs Updates range call to IsInRange.
ai-plans/0043-3-plan-deviations.md Adds plan deviation documentation for generator + hints + messages.
ai-plans/0043-2-openapi-example-messages.md Adds plan doc for example messages feature.
ai-plans/0043-1-improve-documentation-hints.md Adds plan doc for richer hint model.
ai-plans/0043-0-openapi-source-generation.md Adds original plan doc for validator-driven OpenAPI source generation.

feO2x added 3 commits May 22, 2026 07:17
Signed-off-by: Kenny Pflug <kenny.pflug@live.de>
…a order

Signed-off-by: Kenny Pflug <kenny.pflug@live.de>
…orrectly by error category

Signed-off-by: Kenny Pflug <kenny.pflug@live.de>
@github-actions
Copy link
Copy Markdown

Code Coverage

Package Line Rate Branch Rate Complexity Health
Light.PortableResults 96% 94% 2301
Light.PortableResults.AspNetCore.MinimalApis 93% 80% 24
Light.PortableResults.AspNetCore.Mvc 93% 80% 24
Light.PortableResults.AspNetCore.OpenApi 93% 73% 517
Light.PortableResults.AspNetCore.Shared 100% 100% 26
Light.PortableResults.Validation 96% 89% 1757
Light.PortableResults.Validation.OpenApi 97% 88% 103
Light.PortableResults.Validation.OpenApi.SourceGeneration 88% 83% 755
Summary 95% (9340 / 9864) 89% (3942 / 4444) 5507

Minimum allowed line rate is 60%

@feO2x feO2x merged commit 86046ef into main May 22, 2026
2 checks passed
@feO2x feO2x deleted the 43-openapi-source-generation branch May 22, 2026 05:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

OpenAPI Validator-Driven Source Generator

2 participants