Skip to content

Commit 1d1e5be

Browse files
Add aggregate schema test suites and fix recursive allOf stack overflow
Activate parked allOf/anyOf/oneOf test suites into the category-based test structure, port V2-only issue regression tests, and fix a stack overflow when codegen encounters recursive allOf self-references. Test suites moved from _parked/ into active directories: - components/all_of: allOf inheritance chains - components/any_of_inline: inline anyOf response schemas - components/nested_aggregate: allOf+oneOf nesting, arrays of anyOf - parameters/any_of: anyOf/oneOf in query parameters - issues/issue_{193,502,697,775,936,1029,1189,1219,1429,1710,2102} New test suites ported from V2: - issues/issue_1373: recursive allOf self-references - issues/issue_1530: oneOf discriminator with multiple mappings Bug fix (typegen.go): collectFieldsRecursive now tracks visited $refs via a seen set to break cycles when an allOf member references its own parent schema. This prevents the stack overflow reported in #1373. Note: 9 test suites have compilation errors due to V3 naming convention differences (e.g., anyOf members use different field names than the parked tests expected). These will be updated in a follow-up. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 9e64b53 commit 1d1e5be

86 files changed

Lines changed: 7598 additions & 1 deletion

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package: output
2+
output: output/types.gen.go
3+
generation:
4+
runtime-package:
5+
path: github.com/oapi-codegen/oapi-codegen-exp/runtime
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
// Package all_of tests allOf schema composition including inheritance chains,
2+
// required field merging, and nested allOf with additional properties.
3+
package all_of
4+
5+
//go:generate go run ../../../../../cmd/oapi-codegen -config config.yaml spec.yaml

codegen/internal/test/components/all_of/output/types.gen.go

Lines changed: 107 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
package output
2+
3+
import (
4+
"encoding/json"
5+
"testing"
6+
)
7+
8+
// TestAllOfPersonProperties verifies that the PersonProperties type has all
9+
// optional fields generated from the allOf base schema.
10+
// V2 test suite: internal/test/components/allof
11+
func TestAllOfPersonProperties(t *testing.T) {
12+
firstName := "John"
13+
lastName := "Doe"
14+
govID := int64(123456)
15+
16+
pp := PersonProperties{
17+
FirstName: &firstName,
18+
LastName: &lastName,
19+
GovernmentIDNumber: &govID,
20+
}
21+
22+
if *pp.FirstName != "John" {
23+
t.Errorf("FirstName = %q, want %q", *pp.FirstName, "John")
24+
}
25+
if *pp.LastName != "Doe" {
26+
t.Errorf("LastName = %q, want %q", *pp.LastName, "Doe")
27+
}
28+
if *pp.GovernmentIDNumber != 123456 {
29+
t.Errorf("GovernmentIDNumber = %d, want %d", *pp.GovernmentIDNumber, 123456)
30+
}
31+
}
32+
33+
// TestAllOfPerson verifies that the Person type has required first/last name
34+
// fields (non-pointer) and optional GovernmentIDNumber (pointer), reflecting
35+
// the allOf merge with a required-fields schema.
36+
func TestAllOfPerson(t *testing.T) {
37+
govID := int64(999)
38+
p := Person{
39+
FirstName: "Jane",
40+
LastName: "Smith",
41+
GovernmentIDNumber: &govID,
42+
}
43+
44+
if p.FirstName != "Jane" {
45+
t.Errorf("FirstName = %q, want %q", p.FirstName, "Jane")
46+
}
47+
if p.LastName != "Smith" {
48+
t.Errorf("LastName = %q, want %q", p.LastName, "Smith")
49+
}
50+
if *p.GovernmentIDNumber != 999 {
51+
t.Errorf("GovernmentIDNumber = %d, want %d", *p.GovernmentIDNumber, 999)
52+
}
53+
}
54+
55+
// TestAllOfPersonWithID verifies the PersonWithID type which adds an ID field
56+
// via allOf composition on top of PersonProperties.
57+
func TestAllOfPersonWithID(t *testing.T) {
58+
firstName := "Alice"
59+
lastName := "Jones"
60+
govID := int64(555)
61+
62+
pwid := PersonWithID{
63+
FirstName: &firstName,
64+
LastName: &lastName,
65+
GovernmentIDNumber: &govID,
66+
ID: 42,
67+
}
68+
69+
if *pwid.FirstName != "Alice" {
70+
t.Errorf("FirstName = %q, want %q", *pwid.FirstName, "Alice")
71+
}
72+
if pwid.ID != 42 {
73+
t.Errorf("ID = %d, want %d", pwid.ID, 42)
74+
}
75+
}
76+
77+
// TestPersonJSONRoundTrip verifies that Person can be marshaled and
78+
// unmarshaled via JSON with required and optional fields.
79+
func TestPersonJSONRoundTrip(t *testing.T) {
80+
govID := int64(789)
81+
original := Person{
82+
FirstName: "Bob",
83+
LastName: "Brown",
84+
GovernmentIDNumber: &govID,
85+
}
86+
87+
data, err := json.Marshal(original)
88+
if err != nil {
89+
t.Fatalf("Marshal failed: %v", err)
90+
}
91+
92+
var decoded Person
93+
if err := json.Unmarshal(data, &decoded); err != nil {
94+
t.Fatalf("Unmarshal failed: %v", err)
95+
}
96+
97+
if decoded.FirstName != original.FirstName {
98+
t.Errorf("FirstName mismatch: got %q, want %q", decoded.FirstName, original.FirstName)
99+
}
100+
if decoded.LastName != original.LastName {
101+
t.Errorf("LastName mismatch: got %q, want %q", decoded.LastName, original.LastName)
102+
}
103+
if *decoded.GovernmentIDNumber != *original.GovernmentIDNumber {
104+
t.Errorf("GovernmentIDNumber mismatch: got %d, want %d", *decoded.GovernmentIDNumber, *original.GovernmentIDNumber)
105+
}
106+
}
107+
108+
// TestPersonWithIDJSONRoundTrip verifies JSON round-trip for the composed
109+
// PersonWithID type.
110+
func TestPersonWithIDJSONRoundTrip(t *testing.T) {
111+
firstName := "Carol"
112+
lastName := "Davis"
113+
original := PersonWithID{
114+
FirstName: &firstName,
115+
LastName: &lastName,
116+
ID: 100,
117+
}
118+
119+
data, err := json.Marshal(original)
120+
if err != nil {
121+
t.Fatalf("Marshal failed: %v", err)
122+
}
123+
124+
var decoded PersonWithID
125+
if err := json.Unmarshal(data, &decoded); err != nil {
126+
t.Fatalf("Unmarshal failed: %v", err)
127+
}
128+
129+
if *decoded.FirstName != *original.FirstName {
130+
t.Errorf("FirstName mismatch: got %q, want %q", *decoded.FirstName, *original.FirstName)
131+
}
132+
if decoded.ID != original.ID {
133+
t.Errorf("ID mismatch: got %d, want %d", decoded.ID, original.ID)
134+
}
135+
}
136+
137+
// TestPersonRequiredFieldsSerialization verifies that required fields appear in
138+
// JSON even when zero-valued, while optional fields are omitted when nil.
139+
func TestPersonRequiredFieldsSerialization(t *testing.T) {
140+
p := Person{}
141+
data, err := json.Marshal(p)
142+
if err != nil {
143+
t.Fatalf("Marshal failed: %v", err)
144+
}
145+
146+
var m map[string]any
147+
if err := json.Unmarshal(data, &m); err != nil {
148+
t.Fatalf("Unmarshal into map failed: %v", err)
149+
}
150+
151+
// Required fields should be present
152+
if _, ok := m["FirstName"]; !ok {
153+
t.Error("expected FirstName key in JSON output")
154+
}
155+
if _, ok := m["LastName"]; !ok {
156+
t.Error("expected LastName key in JSON output")
157+
}
158+
159+
// Optional field should be absent when nil
160+
if _, ok := m["GovernmentIDNumber"]; ok {
161+
t.Error("expected GovernmentIDNumber to be absent when nil")
162+
}
163+
}
164+
165+
// TestApplyDefaults verifies that ApplyDefaults can be called on all types
166+
// without panic.
167+
func TestApplyDefaults(t *testing.T) {
168+
pp := &PersonProperties{}
169+
pp.ApplyDefaults()
170+
171+
p := &Person{}
172+
p.ApplyDefaults()
173+
174+
pwid := &PersonWithID{}
175+
pwid.ApplyDefaults()
176+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
openapi: "3.0.1"
2+
info:
3+
version: 1.0.0
4+
title: Tests AllOf composition
5+
paths:
6+
/placeholder:
7+
get:
8+
operationId: placeholder
9+
description: |
10+
Validators want at least one path, so this makes them happy.
11+
responses:
12+
default:
13+
description: placeholder
14+
content:
15+
application/json:
16+
schema:
17+
$ref: "#/components/schemas/PersonWithID"
18+
components:
19+
schemas:
20+
PersonProperties:
21+
type: object
22+
description: |
23+
These are fields that specify a person. They are all optional, and
24+
would be used by an `Edit` style API endpoint, where each is optional.
25+
properties:
26+
FirstName:
27+
type: string
28+
LastName:
29+
type: string
30+
GovernmentIDNumber:
31+
type: integer
32+
format: int64
33+
Person:
34+
type: object
35+
description: |
36+
This is a person, with mandatory first and last name, but optional ID
37+
number. This would be returned by a `Get` style API. We merge the person
38+
properties with another Schema which only provides required fields.
39+
allOf:
40+
- $ref: "#/components/schemas/PersonProperties"
41+
- required: [ FirstName, LastName ]
42+
PersonWithID:
43+
type: object
44+
description: |
45+
This is a person record as returned from a Create endpoint. It contains
46+
all the fields of a Person, with an additional resource UUID.
47+
allOf:
48+
- $ref: "#/components/schemas/Person"
49+
- properties:
50+
ID:
51+
type: integer
52+
format: int64
53+
required: [ ID ]
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package: output
2+
output: output/types.gen.go
3+
generation:
4+
runtime-package:
5+
path: github.com/oapi-codegen/oapi-codegen-exp/runtime
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
// Package any_of_inline tests inline anyOf schema composition with response
2+
// schemas containing multiple object variants.
3+
package any_of_inline
4+
5+
//go:generate go run ../../../../../cmd/oapi-codegen -config config.yaml spec.yaml

0 commit comments

Comments
 (0)