feat: add $dynamicRef / $dynamicAnchor schema resolution for OpenAPI 3.1#3889
Open
aqeelat wants to merge 8109 commits into
Open
feat: add $dynamicRef / $dynamicAnchor schema resolution for OpenAPI 3.1#3889aqeelat wants to merge 8109 commits into
aqeelat wants to merge 8109 commits into
Conversation
…t and add biome:check preset Agent-Logs-Url: https://github.com/hey-api/openapi-ts/sessions/c7f5000d-01ad-4791-bd4e-64e753ec4f5f
… intersections When discriminated union members have allOf schemas, they generate ZodIntersection which does not support .extend(). Detect this case by storing isIntersection: true in the symbol meta and fall back to z.union() in tryBuildDiscriminatedUnion. - Add isIntersection field to ZodMeta type - Set isIntersection: true in intersection() handler of all three walkers - Set isIntersection: false in union() handler (manual meta) of all three walkers - Merge ctx.meta with isIntersection flag in processor exportAst calls - Check querySymbol meta for isIntersection in tryBuildDiscriminatedUnion - Add test specs and snapshots for the discriminator-allof-member scenario Agent-Logs-Url: https://github.com/hey-api/openapi-ts/sessions/1c6f992d-8feb-49c9-9cc6-7830259cfe5c
response/request object may become undefined after the changes, but in our test case we can always assert they do exists.
docs: update sponsors copy
chore: add cloudflare spec
chore: update pydantic syntax
refactor: typescript plugin
…nodes refactor: resolver node type
docs: add tanstack start plugin
fix(params): guard __proto__, constructor, and prototype sub-keys
When two anchors PascalCase to the same identifier (e.g. item_type and item-type both produce ItemType), getTemplateParams now appends a numeric suffix to the colliding name instead of emitting duplicates.
…ef bindings Extract containsRefTo helper that walks allOf, anyOf, and oneOf to detect circular references instead of only checking direct $ref and single-level allOf. Add integration fixture with cycle through oneOf and unit tests.
…2.3.2 Reverse spread order in buildCurrentDynamicScope so inherited (outer) scope overrides current (inner) scope on collision, matching the spec's requirement that the outermost matching $dynamicAnchor takes precedence.
…fBinding When materializing a dynamic ref binding, the caller's $defs now merge with the template's $defs instead of wholesale replacing them. This preserves helper sub-schemas that the template may reference from its body alongside the anchor placeholders.
78a2fef to
9a871a9
Compare
- Switch anchorToParamName from PascalCase to sanitized raw anchor casing (e.g. itemType instead of ItemType) to match orval-labs/orval#3353 - Remove circular-binding materialization branch in schema.ts; circular refs now emit generic alias form (type X = Generic<X>) matching Orval - Exclude circular snapshot dirs from tsconfig to suppress TS2456 errors (follow-up: emit interface extends via InterfaceTsDsl) - Update unit tests and integration snapshots
…s to fix TS2456
Add InterfaceTsDsl to ts-dsl and circularTypeAlias flag to IR. When a
circular type alias would produce TS2456 (type X = Generic<X> & {...}),
emit an interface declaration with extends clause instead. Handles
circular refs through allOf; union and naked circular refs remain as
type aliases (known limitation, same as Orval).
… types
Handle TypeExprTsDsl (bare generic reference without intersection) in
exportAst so that patterns like type X = Generic<X> emit as
interface X extends Generic<X> {} instead, avoiding TS2456.
Document that union circular refs (e.g. type X = A | Generic<X>) fall through to type alias because interfaces cannot represent unions. Add comment in export.ts, update spec fixture description, and update test description.
…icRef When a $dynamicRef anchor isn't found in the dynamic scope, scan components.schemas for a schema declaring the same $dynamicAnchor. Per JSON Schema 2020-12, $dynamicRef falls back to $ref behavior when no dynamic scope override exists. Also reorders resolution: typeParams (template placeholders) now take precedence over the components fallback, preventing unbound generic params from being hijacked by unrelated schemas with the same anchor. Ports orval hey-api#3446 and hey-api#3447.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #3886
What changed
$dynamicRef,$dynamicAnchor,$defsto JSON Schema 2020-12 type definitionstypeParamsandtypeArgsfields toIRSchemaObjectdynamicScopeandtypeParamsfields for dynamic anchor → type ref propagation and template parameter tracking through the parserdynamicRef.tsmodule (new file):buildDynamicScope()— builds dynamic scope from own$dynamicAnchorand$defsbindingsbuildCurrentDynamicScope()— merges inherited + current scope; current scope wins on shadowinggetTemplateParams()— detects generic template schemas by finding$defsentries with$dynamicAnchorbut no$refbuildGenericRef()— constructs IR nodes with$ref+typeArgsfor generic type references; preservesnullfrom nullable schemasanchorToParamName()— converts anchor names to valid TypeScript identifiers viatoCase(anchor, 'PascalCase')+ ID_Start/ID_Continue sanitizationmaterializeDynamicRefBinding()— materializes inline$dynamicRefbindingsresolveDynamicRef()— resolves a$dynamicRefstring against the current dynamic scopeshouldInlineDynamicRefTarget()— determines when a$reftarget should be inlinedschema.ts: New$dynamicRefdispatch branch inschemaToIrSchema()— resolves through dynamic scope (recursive/concrete cases) or as a type parameter reference inside generic templates;parseRef()updated to inline dynamic ref targets when scope dictatesinterceptto handle#typeParam/refs (type parameters) and$ref+typeArgs(generic references)exportAstto emit.generic()on type aliases with type parametersdynamicRef.tshelpers + 3 integration test fixtures with snapshots (petstore showcase, external-ref, scope-isolation)Supported patterns
$dynamicAnchor(e.g.BaseCategory.children)Array<unknown>)$defs(e.g.PaginatedTemplate<ItemType>)PaginatedUserResponse)PaginatedTemplate<User>type: ['object', 'null']+$defs)PaginatedTemplate<User> | nullShelterFolder = ShelterFolderTemplate<ShelterFolder, ShelterResource>)$dynamicRefbindingsunknown)item-type)ItemType)Unsupported (falls back to
unknown)$dynamicRef(e.g.other.json#node)$refURIs — external files referenced by$dynamicRefare never fetched. #3902$dynamicAnchor(multiple same-named anchors)Limitations
components.schemasis generated once with a single scope. If the same named component is referenced by multiple endpoints that each provide different$defsbindings, only one binding applies. The common pattern — putting$defsbindings on inline response schemas — works correctly.Design
$dynamicRefpatterns are resolved via dynamic scope materialization (no plugin changes needed). Generic/template$dynamicRefpatterns are preserved as TypeScript generics through IRtypeParams/typeArgsfields with TypeScript visitor support.$dynamicRefis present in the spec. Current behavior for such specs isunknown; any improvement is strictly better.$dynamicRefvalues containing/(JSON pointer fragments) are not resolved.typealiases for generics — Generic templates usetypealiases (notinterface) since ts-dsl already supports generics on aliases and no declaration merging is needed on generated types.toCase(anchor, 'PascalCase')and then sanitized against JavaScriptID_Start/ID_Continuerules to ensure valid TypeScript identifiers (e.g.,item-type→ItemType,folderType→FolderType).