Description
A property whose schema is a $ref to a primitive schema is generated without a
[JsonPropertyName] attribute, unless the referenced schema carries a format keyword.
Since System.Text.Json is case-sensitive by default, the generated client then reads and
writes the wrong JSON member name for those properties: optional ones are silently dropped in
both directions, and required ones throw on deserialization.
Environment
- Package Version: 1.0.2+6665c534b3f1ba93af239b46369d208c6498cd3e
- Operating System: Windows 11 Business 10.0.26200.0
- .NET SDK Version: 10.0.400
Steps to Reproduce
- Save the spec below as
repro.yaml.
- Run:
metaengine-openapi-csharp-httpclient repro.yaml out Repro
- Open
out/Models/Widget.cs: AliasRef and RequiredAliasRef have no
[JsonPropertyName], while InlineString and FormattedRef do.
- Round-trip a spec-conformant payload through
Widget.
Generation succeeds and the output compiles, so there is nothing to notice until runtime.
Expected Behavior
Every property carries the attribute naming its JSON member:
[JsonPropertyName("aliasRef")]
public string? AliasRef { get; init; }
[JsonPropertyName("requiredAliasRef")]
public required string RequiredAliasRef { get; init; }
Actual Behavior
The attribute is missing on both $ref-to-primitive properties, so they bind to "AliasRef"
and "RequiredAliasRef" rather than "aliasRef" and "requiredAliasRef".
Round-tripping step 4 above:
deserializing: {"inlineString":"a","formattedRef":"2026-01-01T00:00:00Z","aliasRef":"b","requiredAliasRef":"c"}
THREW: JSON deserialization for type 'Repro.Models.Widget' was missing required properties including: 'RequiredAliasRef'.
serializing: {"inlineString":"a","formattedRef":null,"AliasRef":"b","RequiredAliasRef":"c"}
A valid response is rejected, and serialization emits mixed casing within one object.
OpenAPI Spec (minimal example)
openapi: 3.0.3
info:
title: JsonPropertyName dropped on $ref-to-primitive
version: 1.0.0
servers:
- url: https://example.test
paths:
/widgets:
get:
tags:
- widget
operationId: getWidget
responses:
'200':
description: ok
content:
application/json:
schema:
$ref: '#/components/schemas/widget'
components:
schemas:
widget:
type: object
required:
- requiredAliasRef
properties:
# Control: inline primitive -> attribute is emitted.
inlineString:
type: string
# Control: $ref to a primitive that HAS `format` -> attribute is emitted.
formattedRef:
$ref: '#/components/schemas/formattedAlias'
# BUG: $ref to a primitive with no `format` -> attribute is NOT emitted.
aliasRef:
$ref: '#/components/schemas/plainAlias'
# BUG: same, and `required` turns silent data loss into a hard throw.
requiredAliasRef:
$ref: '#/components/schemas/plainAlias'
plainAlias:
type: string
formattedAlias:
type: string
format: date-time
Generated Code (if applicable)
out/Models/Widget.cs:
public record Widget
{
[JsonPropertyName("inlineString")]
public string? InlineString { get; init; }
[JsonPropertyName("formattedRef")]
public DateTime? FormattedRef { get; init; }
public string? AliasRef { get; init; } // <- no attribute
public required string RequiredAliasRef { get; init; } // <- no attribute
}
The affected properties are also reordered to the end of the record, out of spec order, which
suggests they are emitted by a different code path from the rest.
out/Models/AliasTypes.cs — both referenced schemas become aliases, so aliasing alone does
not explain the difference:
using PlainAlias = string;
using FormattedAlias = DateTime;
CLI Output / Logs
$ metaengine-openapi-csharp-httpclient repro.yaml out Repro --verbose
Input: repro.yaml
Output: out
Namespace: Repro
[MetaEngine] Starting generation.
[MetaEngine] Files are updated - 10:46:43
[MetaEngine] Total files: 4
[MetaEngine] Created files: 4
[MetaEngine] Updated files: 0
[MetaEngine] Executed successfully.
No warning or diagnostic is produced; --strict-validation does not surface it either.
Additional Context
The discriminator is precisely the presence of format. From a matrix of property shapes:
Attribute emitted: inline primitives; $ref → primitive with format; $ref → enum;
$ref → object; type: array whose items is a $ref to a primitive.
Attribute missing: $ref → type: string, type: integer or type: boolean with no
format. Adding pattern, maxLength or description to the referenced schema does not
help.
Workaround for anyone who finds this: pass a JsonSerializerOptions with
PropertyNamingPolicy = JsonNamingPolicy.CamelCase to the generated clients, since an
explicit [JsonPropertyName] still wins over the policy. That only works while the generated
property names are the PascalCase form of the wire names — it would not save a spec whose
members are snake_case or kebab-case.
Description
A property whose schema is a
$refto a primitive schema is generated without a[JsonPropertyName]attribute, unless the referenced schema carries aformatkeyword.Since
System.Text.Jsonis case-sensitive by default, the generated client then reads andwrites the wrong JSON member name for those properties: optional ones are silently dropped in
both directions, and
requiredones throw on deserialization.Environment
Steps to Reproduce
repro.yaml.metaengine-openapi-csharp-httpclient repro.yaml out Reproout/Models/Widget.cs:AliasRefandRequiredAliasRefhave no[JsonPropertyName], whileInlineStringandFormattedRefdo.Widget.Generation succeeds and the output compiles, so there is nothing to notice until runtime.
Expected Behavior
Every property carries the attribute naming its JSON member:
Actual Behavior
The attribute is missing on both
$ref-to-primitive properties, so they bind to"AliasRef"and
"RequiredAliasRef"rather than"aliasRef"and"requiredAliasRef".Round-tripping step 4 above:
A valid response is rejected, and serialization emits mixed casing within one object.
OpenAPI Spec (minimal example)
Generated Code (if applicable)
out/Models/Widget.cs:The affected properties are also reordered to the end of the record, out of spec order, which
suggests they are emitted by a different code path from the rest.
out/Models/AliasTypes.cs— both referenced schemas become aliases, so aliasing alone doesnot explain the difference:
CLI Output / Logs
No warning or diagnostic is produced;
--strict-validationdoes not surface it either.Additional Context
The discriminator is precisely the presence of
format. From a matrix of property shapes:Attribute emitted: inline primitives;
$ref→ primitive withformat;$ref→ enum;$ref→ object;type: arraywhoseitemsis a$refto a primitive.Attribute missing:
$ref→type: string,type: integerortype: booleanwith noformat. Addingpattern,maxLengthordescriptionto the referenced schema does nothelp.
Workaround for anyone who finds this: pass a
JsonSerializerOptionswithPropertyNamingPolicy = JsonNamingPolicy.CamelCaseto the generated clients, since anexplicit
[JsonPropertyName]still wins over the policy. That only works while the generatedproperty names are the PascalCase form of the wire names — it would not save a spec whose
members are
snake_caseorkebab-case.