Skip to content

Commit 42ff887

Browse files
committed
feat: add api-envelope fixture, showcase cleanup, generics validation, CI hardening
- Add fixtures/api-envelope.yaml testing double-wrapping ApiEnvelopeTemplate<T> pattern with inline route-level binding - Clean up petstore-dynamicref-showcase.yaml: remove ErrorDetail, replace default responses with 400/404, reduce Redocly warnings from 12 to 5 - Add generic template detection to matrix quality analyzer (recognizes parameterized output like PaginatedTemplate<User>) - Fix stale CONCRETE_TYPES (remove BaseFolder/BaseResource, add FolderTemplate, ApiEnvelopeTemplate, PaginatedUserItems) - Mark nested-workspace-resources invalid AJV test as knownGap, fix knownGap label logic to distinguish FIXED from KNOWN-GAP - Wire api-envelope into SCENARIOS, QUALITY_SIGNALS, orval.config.ts, validate-jsonschema.mjs, and GitHub Actions matrix - Update CI to record actual generator status from JSON instead of step outcome - Update docs: fixtures/README.md, state-of-the-union.md, IMPLEMENTATION_GUIDE.md, RUNBOOK.md, README.md, issue template - Update GitHub issues #1 (Orval), #4 (openapi-typescript), #5 (hey-api) with explicit generics validation criteria (PASS/PARTIAL/DEGRADED) - Remove empty fixtures/instances/ directory
1 parent b98c075 commit 42ff887

29 files changed

Lines changed: 2314 additions & 161 deletions

.github/ISSUE_TEMPLATE/generator-tracking.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,19 @@ assignees: ""
1919
- Generation:
2020
- Typecheck:
2121
- `$dynamicRef` fidelity:
22+
- Recursive fixtures: dynamic scope resolves to the active recursive type
23+
- Generic fixtures: generators emit reusable parameterized types when the target language supports generics (concrete materialization is PARTIAL — correct content, lost reuse)
2224
- Last checked:
2325

2426
## Fixture Coverage
2527

2628
- [ ] `baseline-duplicated-pagination`
2729
- [ ] `generic-schema-binding`
2830
- [ ] `paginated-response`
31+
- [ ] `api-envelope`
2932
- [ ] `recursive-category-tree`
3033
- [ ] `nested-workspace-resources`
34+
- [ ] `non-identifier-schema-key`
3135

3236
## Evidence
3337

.github/workflows/matrix.yml

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,10 @@ jobs:
5858
- baseline-duplicated-pagination
5959
- generic-schema-binding
6060
- paginated-response
61+
- api-envelope
6162
- recursive-category-tree
6263
- nested-workspace-resources
64+
- non-identifier-schema-key
6365
version:
6466
- '3.1.0'
6567
- '3.1.1'
@@ -118,6 +120,8 @@ jobs:
118120
- name: Generate SDK
119121
if: steps.select.outputs.selected == 'true'
120122
id: generate
123+
env:
124+
MATRIX_RESULTS_FILE: logs/matrix-results-${{ matrix.tool }}-${{ matrix.scenario }}-${{ matrix.version }}.json
121125
run: node scripts/matrix-runner.mjs --tools=${{ matrix.tool }} --scenarios=${{ matrix.scenario }} --versions=${{ matrix.version }} --no-typecheck --no-analysis
122126
continue-on-error: true
123127

@@ -155,7 +159,12 @@ jobs:
155159
echo "${{ matrix.tool }}.${{ matrix.scenario }}.${{ matrix.version }}.typecheck=filtered" >> "$result_file"
156160
exit 0
157161
fi
158-
gen_outcome="${{ steps.generate.outcome }}"
162+
matrix_result="logs/matrix-results-${{ matrix.tool }}-${{ matrix.scenario }}-${{ matrix.version }}.json"
163+
if [ -f "$matrix_result" ]; then
164+
gen_outcome=$(node -e "const r=require('./${matrix_result}'); console.log(r.results?.[0]?.generate || 'unknown')")
165+
else
166+
gen_outcome="${{ steps.generate.outcome }}"
167+
fi
159168
tc_outcome="${{ steps.typecheck.outcome }}"
160169
tc_skip="${{ steps.typecheck.outputs.skip }}"
161170
echo "${{ matrix.tool }}.${{ matrix.scenario }}.${{ matrix.version }}.generate=${gen_outcome:-skipped}" >> "$result_file"

IMPLEMENTATION_GUIDE.md

Lines changed: 149 additions & 35 deletions
Large diffs are not rendered by default.

README.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ This repository is a public compatibility lab for OpenAPI `$dynamicRef` / `$dyna
66

77
## Current Headline
88

9-
OpenAPI 3.1.x allows JSON Schema 2020-12 schema objects, and OpenAPI 3.2 explicitly recommends dynamic references for generic/template data structures. In the current matrix, tools still either fail to parse `$dynamicAnchor`, generate syntactically valid but semantically degraded TypeScript, or lose concrete dynamic types such as `User[]`, `LocalizedCategory[]`, or `WorkspaceFolder[]`.
9+
OpenAPI 3.1.x allows JSON Schema 2020-12 schema objects, and OpenAPI 3.2 explicitly recommends dynamic references for generic/template data structures. In the current matrix, tools still either fail to parse `$dynamicAnchor`, generate syntactically valid but semantically degraded output (e.g., `unknown`, `any`, or `Object`), lose dynamic scope resolution for recursive types, or materialize generic/template patterns as duplicate concrete types instead of reusable parameterized types.
1010

1111
Generator and typecheck failures in this repo are **report-only** because those failures are the compatibility data. Fixture validity and generated spec freshness are the CI gates.
1212

@@ -56,8 +56,12 @@ Top-level fixtures feed the SDK generator matrix:
5656
| `baseline-duplicated-pagination.yaml` | Control case with duplicated concrete paginated wrappers |
5757
| `generic-schema-binding.yaml` | Generic pagination with named concrete schemas |
5858
| `paginated-response.yaml` | Generic pagination with inline response-level binding |
59+
| `api-envelope.yaml` | Generic response envelope with inline route-level binding |
5960
| `recursive-category-tree.yaml` | Recursive dynamic override using `$dynamicAnchor: category` |
60-
| `nested-workspace-resources.yaml` | Nested resource graph with multiple dynamic anchors |
61+
| `nested-workspace-resources.yaml` | Multi-parameter generic template for nested folder/resource graphs |
62+
| `non-identifier-schema-key.yaml` | Recursive dynamic override with schema keys that need generated identifier normalization |
63+
64+
A combined showcase fixture, [`petstore-dynamicref-showcase.yaml`](petstore-dynamicref-showcase.yaml), exercises all `$dynamicRef` patterns together (generic pagination, response envelopes, nested generics, recursive trees, multi-parameter generic templates, non-identifier keys, typed request/response bodies) and is intended for SDK samples and maintainer demos.
6165

6266
Focused semantics fixtures live under `fixtures/spec-semantics/`. They cover JSON Schema behaviors that are important for parser/validator correctness but are intentionally kept out of the SDK matrix.
6367

RUNBOOK.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ CI also checks `git diff --exit-code -- specs/` after Stage 1. Fixture validity
4141
npm run matrix
4242
```
4343

44-
Runs all 9 generators × 5 scenarios × 4 OAS versions in parallel (default: 8 workers). Generators that aren't installed are skipped with a clear message.
44+
Runs all 9 generators × 7 scenarios × 4 OAS versions in parallel (default: 8 workers). Generators that aren't installed are skipped with a clear message.
4545

4646
### Generators in the Matrix
4747

@@ -116,10 +116,13 @@ scenarios=generic-schema-binding,recursive-category-tree
116116
```bash
117117
grep -n "items\|children" generated/orval/generic-schema-binding/3.1.2/model/paginatedTemplate.ts
118118
grep -n "items\|children" generated/orval/recursive-category-tree/3.1.2/model/baseCategory.ts
119-
grep -n "items\|children" generated/orval/nested-workspace-resources/3.1.2/model/baseFolder.ts
119+
grep -n "children\|shortcuts" generated/orval/nested-workspace-resources/3.1.2/model/folderTemplate.ts
120+
grep -n "data" generated/orval/api-envelope/3.1.2/model/apiEnvelopeTemplate.ts
120121
```
121122

122-
For dynamicRef fixtures, the desired result is concrete types (e.g., `items: User[]`, `children: Category[]`). Current generator output degrades to `unknown[]`, `Array<any>`, or `any`.
123+
For dynamicRef fixtures, the desired result depends on fixture category:
124+
- **Recursive/nested fixtures:** dynamic refs must resolve to the correct active type through dynamic scope (e.g., `children: LocalizedCategory[]`, not `children: unknown[]`).
125+
- **Generic/template fixtures:** generators must emit reusable parameterized types when the target language supports generics (e.g., `PaginatedTemplate<T>` with `PaginatedUserResponse = PaginatedTemplate<User>`). Concrete duplicate wrappers do not pass validation for these fixtures — the point of `$dynamicRef` for generics is type reuse. Current generator output degrades to `unknown[]`, `Array<any>`, `any`, or materializes duplicates instead of parameterized types.
123126

124127
## Optional: JSON Schema Runtime Validation
125128

fixtures/README.md

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,20 @@ npm run validate:fixtures
1313
| Fixture | Purpose | What It Tests |
1414
|---|---|---|
1515
| `baseline-duplicated-pagination.yaml` | Control case with explicit `PaginatedUserResponse` and `PaginatedGroupResponse` schemas | Confirms a generator can handle ordinary duplicated wrappers before testing `$dynamicRef` |
16-
| `generic-schema-binding.yaml` | Reusable paginated wrapper with named concrete schemas (`PaginatedUserResponse`, `PaginatedGroupResponse`) | Tests JSON Schema generic-type pattern using `$dynamicRef` / `$dynamicAnchor` with named type instantiations |
16+
| `generic-schema-binding.yaml` | Reusable paginated wrapper with named concrete schemas (`PaginatedUserResponse`, `PaginatedGroupResponse`) | Tests JSON Schema generic-type pattern using `$dynamicRef` / `$dynamicAnchor` — generators must emit reusable parameterized types when the target language supports generics, not duplicate concrete wrappers |
1717
| `paginated-response.yaml` | Reusable paginated wrapper with type binding at the route response level | Tests the same generic pattern but with `$dynamicAnchor` overrides inline in the path operation response — no separate named wrapper schemas |
18+
| `api-envelope.yaml` | Generic response envelope (`ApiEnvelopeTemplate<T>`) with inline `$defs` binding at the route level — one route binds a single resource, another binds a paginated wrapper | Tests two-level `$dynamicRef` nesting: `ApiEnvelopeTemplate<T>` where `T` is bound inline per route. Generators must emit `ApiEnvelopeTemplate<T>` as a reusable parameterized type. The paginated-list route additionally chains `PaginatedTemplate<User>` as the bound type |
1819
| `recursive-category-tree.yaml` | Canonical dynamic recursive override | Tests dynamic scope for recursive schemas (`children` should use the active category type) |
19-
| `nested-workspace-resources.yaml` | Multiple anchors and nested dynamic refs | Tests more than one `$dynamicAnchor` / `$dynamicRef` pair in a nested resource graph |
20+
| `nested-workspace-resources.yaml` | Multi-parameter generic template for nested folder/resource graphs | Tests a `$dynamicRef` template with two generic slots (`folderType`, `resourceType`) that are bound together in the concrete schema, producing a self-referential folder/resource graph |
21+
| `non-identifier-schema-key.yaml` | Recursive dynamic override with non-identifier schema keys (`base-category`, `localized-category`) | Tests that generators preserve `$dynamicRef` semantics while normalizing schema keys into valid generated identifiers |
22+
23+
A combined showcase fixture at [`petstore-dynamicref-showcase.yaml`](../petstore-dynamicref-showcase.yaml) exercises all `$dynamicRef` patterns together (generic pagination, response envelopes, nested generics, recursive trees, multi-parameter generic templates, non-identifier keys, typed request/response bodies) and is intended for SDK samples, maintainer demos, and "what good output looks like" examples. It is not a replacement for the focused minimal fixtures above.
24+
25+
## Generator Edge Fixtures
26+
27+
Generator edge fixtures are top-level SDK matrix scenarios when the behavior is portable across generators. `non-identifier-schema-key.yaml` is one of these: it tests schema-key normalization plus dynamic reference fidelity, not any one tool's internal implementation.
28+
29+
Tool-specific switches, such as Orval's `enableUnstableDynamicRefSupport`, are not modeled as schema fixtures. Track those in the relevant GitHub issue and tool docs instead.
2030

2131
## Spec Semantics Fixtures
2232

@@ -25,6 +35,8 @@ npm run validate:fixtures
2535
| Fixture | What It Tests |
2636
|---|---|
2737
| `spec-semantics/dynamicref-core-semantics.yaml` | Same-resource dynamic anchors, `$dynamicRef` to `$anchor`, `$ref` to `$dynamicAnchor`, fallback to ordinary anchor behavior, non-fragment URI dynamic refs, multi-parameter generic binding, and allOf sibling order |
38+
| `spec-semantics/external-dynamic-ref.yaml` | `$dynamicRef` to an external JSON Schema resource (`external-dynamic-target.json`) for parser/bundler research |
39+
| `spec-semantics/ambiguous-sibling-anchors.yaml` | Multiple sibling schemas with the same `$dynamicAnchor` name, documenting where static sibling scans are only an approximation of dynamic scope |
2840

2941
## Why Keep The Baseline?
3042

@@ -63,9 +75,13 @@ Run via `scripts/validate-openapi.sh` as part of the Stage 1 pipeline. This incl
6375
| `baseline-duplicated-pagination.yaml` | Pass | Pass | Pass | Pass |
6476
| `generic-schema-binding.yaml` | Pass | Pass | Pass | Pass |
6577
| `paginated-response.yaml` | Pass | Pass | Pass | Pass |
78+
| `api-envelope.yaml` | Pass | Pass | Pass | Pass |
6679
| `recursive-category-tree.yaml` | Pass | Pass | Pass | Pass |
6780
| `nested-workspace-resources.yaml` | Pass | Pass | Pass | Pass |
81+
| `non-identifier-schema-key.yaml` | Pass | Pass | Pass | Pass |
82+
| `spec-semantics/ambiguous-sibling-anchors.yaml` | Pass | Pass | Pass | Pass |
6883
| `spec-semantics/dynamicref-core-semantics.yaml` | Pass | Pass | Pass | Pass |
84+
| `spec-semantics/external-dynamic-ref.yaml` | Pass | Pass | Pass | Pass |
6985

7086
## JSON Schema Runtime Validation
7187

@@ -78,11 +94,15 @@ Validators: AJV 2020 and Hyperjump 2020-12.
7894
| `baseline-duplicated-pagination.yaml` | Pass | Pass | Not tested | Control passes |
7995
| `generic-schema-binding.yaml` | Valid user/group pages fail; invalid item cases fail | Valid user/group pages pass; invalid item cases fail | Valid user/group pages pass; invalid item cases fail | AJV PR #2615 fixes this |
8096
| `paginated-response.yaml` | Valid user/group pages fail; invalid item cases fail | Valid user/group pages pass; invalid item cases fail | Valid user/group pages pass; invalid item cases fail | AJV PR #2615 fixes this |
97+
| `api-envelope.yaml` | Valid wrapped-user page fails; invalid item cases fail | Not tested | Not tested | Expected same gap as pagination fixtures; AJV PR #2615 should fix this |
8198
| `recursive-category-tree.yaml` | Valid localized category tree passes; child missing localized fields fails | Pass | Not tested | Runtime check passes |
82-
| `nested-workspace-resources.yaml` | Valid nested workspace passes; nested folder missing permissions fails | Fails: "resolves to more than one schema" for multiple same-name `$dynamicAnchor` | Not tested | Separate AJV gap: multiple schemas with same `$dynamicAnchor` name |
99+
| `nested-workspace-resources.yaml` | Valid nested workspace passes; AJV accepts invalid nested folder data (missing `permissions` field) | Fails: "resolves to more than one schema" for multiple same-name `$dynamicAnchor` in the same document (`FolderTemplate.$defs.folderType` and `WorkspaceFolder.$defs.folderType` share an anchor name) — this is an AJV limitation, not a fixture bug | Not tested | AJV does not correctly resolve the multi-parameter generic template pattern; the fixture is semantically sound |
100+
| `non-identifier-schema-key.yaml` | Valid localized category tree passes; child missing localized fields fails | Not tested | Valid localized category tree passes; child missing localized fields fails | Runtime check passes; generator matrix tests identifier normalization |
101+
| `spec-semantics/ambiguous-sibling-anchors.yaml` | Not tested | Not tested | Not tested | OpenAPI-valid research fixture for static-scan limitations |
83102
| `spec-semantics/dynamicref-core-semantics.yaml` | `$ref` to `$dynamicAnchor` passes; several `$dynamicRef` core cases are known gaps, including non-fragment URI refs | Not tested | Core cases, non-fragment URI refs, allOf order, and multi-parameter generic cases pass | Semantics fixture tier; AJV gaps are documented by case output |
103+
| `spec-semantics/external-dynamic-ref.yaml` | Not tested | Not tested | Valid external node passes; invalid external node fails | External resource semantics fixture for parser/bundler research |
84104

85-
AJV PR [#2615](https://github.com/ajv-validator/ajv/pull/2615) fixes the generic pagination dynamic binding pattern. The nested workspace fixture exposes a separate AJV limitation: multiple schemas declaring the same `$dynamicAnchor` name in a single document causes an ambiguous reference error.
105+
AJV PR [#2615](https://github.com/ajv-validator/ajv/pull/2615) fixes the generic pagination dynamic binding pattern. The nested workspace fixture exposes a separate AJV limitation: multiple schemas declaring the same `$dynamicAnchor` name in a single document causes an ambiguous reference error. This is a limitation of AJV PR #2615, not a bug in the fixture — `nested-workspace-resources.yaml` is semantically correct (both anchor declarations are scoped to different schemas in the `allOf` chain).
86106

87107
## Fixture To Spec Path
88108

fixtures/api-envelope.yaml

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
openapi: 3.1.0
2+
info:
3+
title: DynamicRef API Envelope API
4+
description: >
5+
Tests the double-wrapping $dynamicRef pattern: a generic response envelope
6+
(ApiEnvelopeTemplate<T>) whose data slot is bound at the route response level
7+
using inline $defs + $dynamicAnchor. Both a single-resource and a paginated
8+
resource response are included to exercise two distinct bindings against the
9+
same template.
10+
version: 0.1.0
11+
license:
12+
name: MIT
13+
identifier: MIT
14+
servers:
15+
- url: https://api.dynamicref.test
16+
security: []
17+
18+
paths:
19+
/users/{userId}:
20+
get:
21+
summary: Get a user
22+
operationId: getUser
23+
tags: [Users]
24+
parameters:
25+
- name: userId
26+
in: path
27+
required: true
28+
schema:
29+
type: string
30+
format: uuid
31+
responses:
32+
'200':
33+
description: A single user wrapped in the response envelope
34+
content:
35+
application/json:
36+
schema:
37+
$defs:
38+
dataType:
39+
$dynamicAnchor: dataType
40+
$ref: '#/components/schemas/User'
41+
$ref: '#/components/schemas/ApiEnvelopeTemplate'
42+
'404':
43+
description: Not found
44+
45+
/users:
46+
get:
47+
summary: List users
48+
operationId: listUsers
49+
tags: [Users]
50+
parameters:
51+
- name: page
52+
in: query
53+
schema:
54+
type: integer
55+
minimum: 1
56+
default: 1
57+
- name: pageSize
58+
in: query
59+
schema:
60+
type: integer
61+
minimum: 1
62+
maximum: 100
63+
default: 20
64+
responses:
65+
'200':
66+
description: Paginated user list wrapped in the response envelope
67+
content:
68+
application/json:
69+
schema:
70+
$defs:
71+
dataType:
72+
$dynamicAnchor: dataType
73+
$ref: '#/components/schemas/PaginatedUserItems'
74+
$ref: '#/components/schemas/ApiEnvelopeTemplate'
75+
'400':
76+
description: Bad request
77+
78+
components:
79+
schemas:
80+
User:
81+
type: object
82+
required: [id, email]
83+
properties:
84+
id:
85+
type: string
86+
format: uuid
87+
email:
88+
type: string
89+
format: email
90+
91+
ApiEnvelopeTemplate:
92+
$id: https://example.com/schemas/ApiEnvelopeTemplate
93+
$defs:
94+
dataType:
95+
$dynamicAnchor: dataType
96+
not: {}
97+
type: object
98+
required: [data, requestId]
99+
properties:
100+
data:
101+
$dynamicRef: '#dataType'
102+
requestId:
103+
type: string
104+
format: uuid
105+
106+
PaginatedTemplate:
107+
$id: https://example.com/schemas/PaginatedTemplate
108+
$defs:
109+
itemType:
110+
$dynamicAnchor: itemType
111+
not: {}
112+
type: object
113+
required: [items, total, page, pageSize]
114+
properties:
115+
items:
116+
type: array
117+
items:
118+
$dynamicRef: '#itemType'
119+
total:
120+
type: integer
121+
minimum: 0
122+
page:
123+
type: integer
124+
minimum: 1
125+
pageSize:
126+
type: integer
127+
minimum: 1
128+
129+
PaginatedUserItems:
130+
$defs:
131+
itemType:
132+
$dynamicAnchor: itemType
133+
$ref: '#/components/schemas/User'
134+
$ref: '#/components/schemas/PaginatedTemplate'

0 commit comments

Comments
 (0)