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
-
Published specs state the wrong contract. Any consumer reading the OpenAPI sees a number where the API returns a string.
-
Precision is silently lost. 50000.00 round-trips to 50000, which matters for money fields.
-
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.
Summary
protoc-gen-openapiv3writesfield_examplesvalues as YAML scalar nodes without a tag, so the YAML encoder applies implicit type resolution. An example on astringfield 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:
Minimal proof of the mechanism, run against
main(a2b2e8e):!!str"50000.00"50000.00(float)"50000.00""2024-01-01T00:00:00Z""2024-01-01T00:00:00Z""ACTIVE"ACTIVEACTIVE(not over-quoted)"010203ABCD"010203ABCD010203ABCD(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 — theschema.Example/schema.Examplesconstruction at ~L228-243 and ~L520-532:Impact
Published specs state the wrong contract. Any consumer reading the OpenAPI sees a number where the API returns a string.
Precision is silently lost.
50000.00round-trips to50000, which matters for money fields.Spec-driven mocking is unusable for affected services. Serving
alpaca-go's generated specs with Prism returns{"equity": 50000}as a JSON number;protojson.Unmarshalthen rejects it outright: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 carryfield_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 —
!!strfor string fields, and correspondingly for numeric/bool kinds so genuinely numeric examples stay numeric. Both sites ininternal/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.