Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
c848f6e
test(downgrader): cover the cut-$ref reset, writeOnly and $ref tags, …
claude Nov 2, 2026
17b0cc9
Merge remote-tracking branch 'origin/main' into claude/project-thread…
claude Nov 2, 2026
90d9ae5
fix(downgrader): leave out a 3.2 security requirement name whose sche…
claude Nov 2, 2026
71b348d
Merge remote-tracking branch 'origin/main' into claude/project-thread…
claude Nov 2, 2026
720cf6e
test(downgrader): cover a 3.2 security requirement name holding undef…
claude Nov 2, 2026
de5d602
Merge remote-tracking branch 'origin/main' into claude/project-thread…
claude Nov 2, 2026
6c5d811
Merge remote-tracking branch 'origin/main' into claude/project-thread…
claude Nov 2, 2026
85e0979
test(downgrader): cover a null branch beside enums in a malformed anyOf
claude Nov 2, 2026
dd95369
Merge remote-tracking branch 'origin/main' into claude/project-thread…
claude Nov 2, 2026
170d121
Merge remote-tracking branch 'origin/main' into claude/project-thread…
claude Nov 2, 2026
4ea0add
test(downgrader): keep the allOf beside a restricting keyword on a ta…
claude Nov 2, 2026
eef3ee2
Merge remote-tracking branch 'origin/main' into claude/project-thread…
claude Nov 2, 2026
c7b4e1b
Merge remote-tracking branch 'origin/main' into claude/project-thread…
claude Nov 2, 2026
7531a72
docs(downgrader): a $ref into an anyOf removed for a media-type-only …
claude Nov 2, 2026
4c4b7bf
Merge remote-tracking branch 'origin/main' into claude/project-thread…
claude Nov 2, 2026
946a4f6
docs(downgrader): a $ref into a removed not dangles
claude Nov 2, 2026
ae2be47
Merge remote-tracking branch 'origin/main' into claude/project-thread…
claude Nov 2, 2026
69c1aae
docs(downgrader): a $ref into a 3.2 itemSchema that moves into schema…
claude Nov 2, 2026
91533fb
Merge remote-tracking branch 'origin/main' into claude/project-thread…
claude Nov 2, 2026
f4f0c9b
docs(downgrader): a $ref by index past a removed 3.2 querystring para…
claude Nov 2, 2026
74e3b3b
Merge remote-tracking branch 'origin/main' into claude/project-thread…
claude Nov 2, 2026
2360af7
Merge remote-tracking branch 'origin/main' into claude/project-thread…
claude Nov 2, 2026
25fb9ca
fix(downgrader): keep a $ref bare beside only a 3.0-style nullable
claude Nov 2, 2026
5658ff8
Merge remote-tracking branch 'origin/main' into claude/project-thread…
claude Nov 2, 2026
ffa455e
fix(downgrader): keep a $ref bare beside identifiers 3.0 removes
claude Nov 2, 2026
114cae7
Merge remote-tracking branch 'origin/main' into claude/project-thread…
claude Nov 2, 2026
24cfba2
fix(downgrader): keep null in a loop of nullable $ref pairs
claude Nov 2, 2026
bc1fe15
fix(downgrader): follow nullable pairs beside annotations 3.0 drops
claude Nov 2, 2026
f3cc671
Merge remote-tracking branch 'origin/main' into claude/project-thread…
claude Nov 2, 2026
47d2d6c
Merge remote-tracking branch 'origin/main' into claude/project-thread…
claude Nov 2, 2026
f7cdda7
fix(downgrader): drop a discriminator that a $ref or type list would …
claude Nov 2, 2026
660afb1
docs(downgrader): note the discriminator dropped beside a $ref or typ…
claude Nov 2, 2026
6718cbd
fix(downgrader): keep a $ref bare beside only a removed discriminator
claude Nov 2, 2026
272eaf2
Merge remote-tracking branch 'origin/main' into claude/project-thread…
claude Nov 2, 2026
80c69ae
test(downgrader): keep a discriminator beside a $ref and an anyOf or …
claude Nov 2, 2026
a8c2c54
Merge remote-tracking branch 'origin/main' into claude/project-thread…
claude Nov 2, 2026
c80c90c
fix(downgrader): follow a null pair nested in a null pair around a $r…
claude Nov 2, 2026
e47c94c
Merge remote-tracking branch 'origin/main' into claude/project-thread…
claude Nov 2, 2026
96fe14d
fix(downgrader): stop the nested pair walk at a cycle of objects
claude Nov 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 20 additions & 2 deletions packages/downgrader/src/v3.1-to-v3.0.ts
Original file line number Diff line number Diff line change
Expand Up @@ -962,7 +962,8 @@ function admitsNull(schema: unknown): boolean {
// becomes. A target that holds such a pair, beside annotations that 3.0 may
// drop, such as `$comment`, can become its own `$ref` in turn, so a loop of
// them, which in 3.1 admits only null, would become `$ref`s that match
// nothing. One that does not admit null keeps its pair.
// nothing. One that does not admit null keeps its pair. A pair nested in the
// target's `anyOf`, as `anyOf: [{ anyOf: [$ref, null] }, null]`, leads on too.
function refAdmitsNull(ref: string, ctx: Context): boolean {
const seen = new Set<string>()
for (let next: string | undefined = ref; next !== undefined;) {
Expand All @@ -975,12 +976,29 @@ function refAdmitsNull(ref: string, ctx: Context): boolean {
}
seen.add(next)
const { anyOf } = target as Record<string, unknown>
const others = Array.isArray(anyOf) ? anyOf.filter((branch) => !acceptsNull(branch)) : []
const others = Array.isArray(anyOf) ? nonNullBranches(anyOf) : []
next = others.length === 1 ? getRef(others[0]) : undefined
}
return true
}

// Met again, around a cycle of objects, a union adds nothing.
function nonNullBranches(branches: unknown[], seen = new Set<unknown[]>()): unknown[] {
if (seen.has(branches)) {
return []
}
seen.add(branches)
return branches.flatMap((branch) => {
if (acceptsNull(branch)) {
return []
}
const nested = isRecord(branch) ? (branch.anyOf ?? branch.oneOf) : undefined
return Array.isArray(nested) && nested.some(acceptsNull)
? nonNullBranches(nested, seen)
: [branch]
})
}

// A `type` with no `nullable: true` beside it, in 3.1 or 3.0, rejects null.
function rejectsNull(schema: unknown): boolean {
if (!isRecord(schema) || schema.nullable === true) {
Expand Down
20 changes: 20 additions & 0 deletions packages/downgrader/tests/v3.1-to-v3.0.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1374,6 +1374,26 @@ describe('downgradeSchemaV31ToV30', () => {
A: { anyOf: [ref('B'), onlyNull] },
B: { description: 'B', anyOf: [ref('A'), onlyNull] },
})
// So does a pair nested in the target's union.
const nested = { anyOf: [{ anyOf: [ref('A'), onlyNull] }, onlyNull] }
expect(schemas({ A: { anyOf: [pair('A'), { type: 'null' }] } })).toEqual({ A: nested })
expect(schemas({ A: { anyOf: [{ oneOf: pair('A').anyOf }, { type: 'null' }] } })).toEqual({
A: nested,
})
expect(
schemas({
A: pair('B'),
B: { anyOf: [{ anyOf: [ref('A'), ref('B')] }, { type: 'null' }] },
}),
).toEqual({ A: ref('B'), B: { anyOf: [{ anyOf: [ref('A'), ref('B')] }, onlyNull] } })
expect(schemas({ A: pair('B'), B: { anyOf: [false, ref('A'), { type: 'null' }] } })).toEqual({
A: ref('B'),
B: { anyOf: [{ not: {} }, ref('A'), onlyNull] },
})
// A cycle of objects ends the walk.
const cyclic: Record<string, any> = { anyOf: [{ type: 'null' }] }
cyclic.anyOf.unshift({ anyOf: [cyclic, { type: 'null' }] })
expect(schemas({ A: pair('B'), B: cyclic })?.A).toEqual(ref('B'))
})

it('merges a branch in full where a cycle meets it before its conversion is done', () => {
Expand Down
Loading