feat(codegen): support non-conforming OpenAPI property keys in generated models - #67
Merged
Merged
Conversation
…operty keys OpenAPI property keys are normalized to PascalCase C# names, which silently breaks System.Text.Json binding whenever the original key does not round-trip through the default camelCase naming policy - for example underscores, dots and custom acronym casing such as `MyEnergyData_MarketDocument`, `sender_MarketParticipant.name`, `period.timeInterval` and `mRID`. Add JsonPropertyNameHelper with: - RequiresJsonPropertyName(jsonKey, csharpPropertyName), returning false for an exact ordinal match or when the key equals the camelCased property name, and true otherwise. - CreateJsonPropertyNameAttribute(jsonKey), producing the AttributeParameters used by the record generators. No call sites yet; wiring follows in a later commit.
…roperty keys Wire JsonPropertyNameHelper into every record extraction path so generated models bind the original OpenAPI wire keys instead of the normalized C# names. - SchemaExtractor: ExtractGenericPaginatedRecord, ExtractRecordFromSchema and ExtractRecordFromSchemaWithInlineEnums now build a combined attribute list, with the JsonPropertyName attribute first so validation attributes append cleanly, and pass null when the list ends up empty. - SchemaExtractor.BuildHeaderContent adds `using System.Text.Json.Serialization;` when any emitted parameter carries the attribute. - InlineSchemaExtractor: both extraction sites delegate to a new private BuildAttributes helper mirroring the SchemaExtractor behaviour. The raw `prop.Key` is always used as the JSON key, so properties renamed to avoid a collision with their enclosing type (Status -> StatusValue) stay correctly mapped.
…sed model path Using directives are resolved independently by the two generation paths: SchemaExtractor.BuildHeaderContent for the source generator, and GeneratedType.RequiredUsings for the CLI/file-based path in CodeGenerationService.GenerateModels. Only the former had been updated, so models emitted through the CLI referenced JsonPropertyName without importing it and failed to compile. Add UsingStatementHelper.RecordUsesJsonPropertyName(record) to detect the attribute on the record model, and consume it in GenerateModels alongside the existing RecordUsesSystemTypes check.
- JsonPropertyNameGenerationTests: drive CodeGenerationService.GenerateModels end to end and assert both the exact emitted parameter text and that System.Text.Json.Serialization lands in GeneratedType.RequiredUsings. Note that GeneratedType.Content holds the type body only, so using directives must be asserted against RequiredUsings. - RecordSerializationTests: prove the generated contract actually serializes and deserializes, using records that mirror the generator output.
…payload
Capture a production response body from
POST /thirdpartyapi/api/meterdata/gettimeseries/{dateFrom}/{dateTo}/{aggregation} and
use it as a regression fixture. The payload is dense with non-conforming keys -
MyEnergyData_MarketDocument, mRID, sender_MarketParticipant.mRID, period.timeInterval,
measurement_Unit.name and out_Quantity.quantity - across a five-level object graph.
- GeneratedModels_CanMapEveryPropertyKeyInRealPayload runs SchemaExtractor over the real
api-1.yaml, derives the set of bindable keys from the emitted JsonPropertyName
attributes plus default name matching, and asserts no payload key is left unmapped.
- Three further tests assert deserialization values, that serialization re-emits the
original wire keys, and that a full round-trip preserves the 96-point series.
Wire the payload and a link to the sample specification into the test project as
copy-to-output content.
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.
Summary
Generated models normalize OpenAPI property keys to PascalCase C# names. When the original key
does not round-trip through the default camelCase naming policy,
System.Text.Jsonbindingsilently breaks — properties deserialize as
nullwith no error at compile time or runtime.This affects any spec using underscores, dots or custom acronym casing in property keys. The
Eloverblik API in
sample/ThridParty-Typed-Clients/EloverblikThirdPartyApiClientis areal-world example:
MyEnergyData_MarketDocumentMyEnergyDataMarketDocumentsender_MarketParticipant.nameSenderMarketParticipantNamesender_MarketParticipant.mRIDSenderMarketParticipantMRidperiod.timeIntervalPeriodTimeIntervalmeasurement_Unit.nameMeasurementUnitNameout_Quantity.quantityOutQuantityQuantitymRIDMRidcreatedDateTimeCreatedDateTimeThe fix emits
[property: JsonPropertyName("<original key>")]whenever the original key does notmatch the default convention, leaving conforming keys untouched so existing output is unchanged.
Changes
Detection — new
JsonPropertyNameHelperdecides whether a property needs an explicitattribute:
falseon an exact ordinal match or when the key equals the camelCased property name,trueotherwise.Emission — wired into all five record extraction paths:
ExtractGenericPaginatedRecord,ExtractRecordFromSchemaandExtractRecordFromSchemaWithInlineEnumsinSchemaExtractor, plusboth entry points in
InlineSchemaExtractorvia a sharedBuildAttributeshelper. TheJsonPropertyNameattribute is placed first so validation attributes append cleanly.Using directives — resolved independently by the two generation paths, so both needed
updating:
SchemaExtractor.BuildHeaderContentCodeGenerationService.GenerateModelsvia a newUsingStatementHelper.RecordUsesJsonPropertyNameThe second was a genuine latent bug — CLI-generated models referenced
JsonPropertyNamewithoutimporting it and failed to compile. It was only surfaced by writing the integration test.
Example output
Testing
JsonPropertyNameHelperTestsGenerateContentForRecordsTestsAtc.CodeGeneration.CSharprenders property-targeted attributes, alone and combined with validation attributesSchemaExtractorJsonPropertyNameTests,InlineSchemaExtractorTestsJsonPropertyNameGenerationTestsCodeGenerationService.GenerateModelsoutput, includingRequiredUsingsRecordSerializationTestsMeterDataGetTimeSeriesPayloadTestsThe last one is the strongest guard. It uses an actual response body from
POST /thirdpartyapi/api/meterdata/gettimeseries/{dateFrom}/{dateTo}/{aggregation}— a five-levelobject graph with 96 quarter-hour data points.
GeneratedModels_CanMapEveryPropertyKeyInRealPayloadruns
SchemaExtractorover the realapi-1.yaml, derives the set of bindable keys from the emittedattributes, recursively collects every key in the payload and asserts none are left unmapped. This
ties the fixture directly to generator output rather than to hand-written mirrors, so it will fail
if the generator ever regresses.
Full suite green. Sample
EloverblikThirdPartyApiClientbuilds with 0 warnings and emits theexpected attributes.
Backwards compatibility
No change for specs whose property keys already follow camelCase or PascalCase conventions — the
helper returns
falseand no attribute is emitted. Specs that previously produced silently brokenmodels will now produce correct ones, changing generated output but only where it was already wrong.
Commits
17ccdf95feat(codegen): addJsonPropertyNameHelper3ebf9036feat(codegen): emit the attribute across extraction pathsa00319d6fix(codegen):System.Text.Json.Serializationusing on the file-based path86c541f2test(codegen): round-trip verificatione40dfbectest(codegen): real Eloverblik payload guard