Skip to content

openapiv3: field_examples on string fields emit untagged YAML scalars, changing their type #244

Description

@SebastienMelki

Summary

protoc-gen-openapiv3 writes field_examples values as YAML scalar nodes without a tag, so the YAML encoder applies implicit type resolution. An example on a string field whose value looks numeric (or like a timestamp) is emitted unquoted and is therefore no longer a string in the generated spec.

Reproduction

Proto (correct as written):

string last_equity = 7 [(sebuf.http.field_examples) = { values: ["50000.00"] }];
string created_at  = 8 [(sebuf.http.field_examples) = { values: ["2024-01-01T00:00:00Z"] }];

Generated OpenAPI:

lastEquity:
    type: string
    examples:
        - 50000.00              # YAML float, not a string
createdAt:
    type: string
    examples:
        - 2024-01-01T00:00:00Z  # YAML timestamp, not a string

Minimal proof of the mechanism, run against main (a2b2e8e):

n := &yaml.Node{Kind: yaml.ScalarNode, Value: "50000.00"} // current code
yaml.Marshal(n) // => 50000.00

n := &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: "50000.00"}
yaml.Marshal(n) // => "50000.00"
annotation value current with !!str
"50000.00" 50000.00 (float) "50000.00"
"2024-01-01T00:00:00Z" unquoted (timestamp) "2024-01-01T00:00:00Z"
"ACTIVE" ACTIVE ACTIVE (not over-quoted)
"010203ABCD" 010203ABCD 010203ABCD (not over-quoted)

Tagging is safe: the encoder still omits quotes where the value is unambiguously a string, so this is not a formatting churn change.

Root cause

internal/openapiv3/types.go, two sites — the schema.Example / schema.Examples construction at ~L228-243 and ~L520-532:

schema.Example = &yaml.Node{
    Kind:  yaml.ScalarNode,
    Value: examples[0],   // no Tag -> implicit resolution
}

Impact

  1. Published specs state the wrong contract. Any consumer reading the OpenAPI sees a number where the API returns a string.

  2. Precision is silently lost. 50000.00 round-trips to 50000, which matters for money fields.

  3. Spec-driven mocking is unusable for affected services. Serving alpaca-go's generated specs with Prism returns {"equity": 50000} as a JSON number; protojson.Unmarshal then rejects it outright:

    {"name":50000}      -> err=invalid value for string field name: 50000
    {"name":"50000.00"} -> err=<nil>
    

    So the generated client cannot decode responses mocked from its own spec.

Hit while evaluating Prism as a local double for alpaca-go (76-92% of its schema properties carry field_examples, so this affects a large share of them).

Proposed fix

Set the scalar tag from the proto field kind when building the example nodes — !!str for string fields, and correspondingly for numeric/bool kinds so genuinely numeric examples stay numeric. Both sites in internal/openapiv3/types.go.

No golden currently covers a string field with a numeric-looking example, which is why this went unnoticed — worth adding one alongside the fix.

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

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions