Skip to content

[BUG] Missing JsonPropertyName attributes #1

Description

@nlqivision

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

  1. Save the spec below as repro.yaml.
  2. Run: metaengine-openapi-csharp-httpclient repro.yaml out Repro
  3. Open out/Models/Widget.cs: AliasRef and RequiredAliasRef have no
    [JsonPropertyName], while InlineString and FormattedRef do.
  4. 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: $reftype: 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions