Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion packages/downgrader/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ Accepts the default OAS dialect, which adds `discriminator`, `xml`, `externalDoc
| An [`anyOf`][js-any-of] of `{ type: "null" }` and one other branch, as Zod writes a nullable object, array, integer, enum, unknown, or `$ref`, Zod and Valibot a nullable nullish one with such a pair as the other branch, and Effect a nullish one with `{ type: "null" }` twice | That branch with `nullable: true` in place of the `anyOf` when it has a single `type`, since code generators read the pair as a union. A branch with a `$ref`, an `enum`, `allOf`, `anyOf`, `oneOf`, or `not` stays, since `nullable` would not let null through those, and so does one that holds a `type` where the schema around it holds one, or another keyword that schema holds with another value, or holds `properties` or `additionalProperties` where that schema holds the other, since `additionalProperties` reads the `properties` beside it. The pair becomes a `oneOf` when that branch has a `type` and no `nullable`, or is a `$ref` to such a schema, alone or with siblings such as the `description` Zod writes beside a described reference, since null then matches one branch only, and NSwag reads that as nullable but the `anyOf` as an empty class. A `$ref` to a schema whose `type` already holds `"null"`, as GitHub writes, takes the place of the `anyOf` when nothing else sits beside it, as 3.0 writes a nullable reference. A branch that accepts every value, such as `{}`, which Zod writes for a nullable unknown and Pydantic for `Optional[Any]`, removes the `anyOf`, since the pair then accepts every value too, and code generators read it as a union with a string or as a type they never define. Its annotations, such as the `description` Zod writes there for a described unknown, take its place, unless the schema around it has one of them with another value. A null branch whose other keywords repeat the schema around it, as ArkType writes the description of a described nullable value into every branch, counts as `{ type: "null" }`. |
| A [`oneOf`][js-one-of] of `{ type: "null" }` and one other branch, as typia and Litestar write a nullable value | That branch with `nullable: true` in place of the `oneOf`, under the same conditions as the `anyOf` pair above but only with a single null branch and a branch without `nullable`, since null then matches one branch only. Otherwise the `oneOf` stays. |
| An [`anyOf`][js-any-of] of scalar types, or of them and `"array"`, as ArkType, Valibot and Pydantic write a union of them and Effect a number, and a [`type`][js-type] list of them, as Zod writes it and ASP.NET Core a number that may come as a string | A `oneOf` in place of the `anyOf` when each branch has a scalar or `"array"` `type` of its own and is not `nullable`, they do not name both `"integer"` and `"number"`, and no `enum` or `oneOf` sits beside them, since no value then matches two branches, with null in it once where Effect writes it twice for a nullish number. Such a `oneOf` with nothing beside it but annotations, as a nullable union of them becomes, joins the branches around it, and its annotations, such as the `description` Zod writes there for a described union, move to the schema around it, unless that schema or another such branch has one of them with another value. NSwag reads that `oneOf` as the first branch, and the OpenAPI Generator TypeScript client as a union, but the `anyOf` as an empty class. A schema a `$ref` points to keeps the `anyOf`, since NSwag gives a property a type it never defines when an `allOf` holds a `$ref` to a `oneOf`, as a `$ref` with siblings becomes. |
| [`const`][js-const] | `enum` with its one value, and the `type` of that value when it has none, since code generators drop or leave untyped a bare `enum`. `const: null` becomes the same schema as `type: "null"`. A union whose branches all become enums of one `type`, as OpenSearch writes a documented enum, gets that `type` beside it too, since NSwag otherwise reads it as an empty class or as a type it never defines. When those enums hold nothing else, as Zod, Valibot, TypeBox and GitHub write a union of literals, or only keywords the schema around them holds with the same value, as ArkType writes a described one, they become one `enum`, beside any null branch, since code generators read the union as a string or as a union of one-value types. A `oneOf` whose branches share a value stays. |
| [`const`][js-const] | `enum` with its one value, and the `type` of that value when it has none, since code generators drop or leave untyped a bare `enum`. `const: null` becomes the same schema as `type: "null"`. A union whose branches all become enums of one `type`, as OpenSearch writes a documented enum, gets that `type` beside it too, since NSwag otherwise reads it as an empty class or as a type it never defines. When those enums hold nothing else, as Zod, Valibot, TypeBox and GitHub write a union of literals, or only keywords the schema around them holds with the same value, as ArkType writes a described one, they become one `enum`, beside any null branch, since code generators read the union as a string or as a union of one-value types. Beside branches of other types, as Zod, ArkType and Valibot write a union of string literals and `number`, and utoipa the unit variants of a Rust enum, the enums of each type become one, and a union of scalars then becomes a `oneOf`. A `oneOf` whose branches share a value stays. |
| An [`enum`][js-enum] with no `type`, as ArkType writes a union of literals | The `type` its values share, when they share one, since code generators drop or leave untyped a bare `enum`, or read its integers as numbers. |
| Numeric [`exclusiveMinimum`][js-exclusive-minimum] and [`exclusiveMaximum`][js-exclusive-maximum] | `minimum` or `maximum` with a boolean `exclusiveMinimum` or `exclusiveMaximum`, unless `minimum` or `maximum` is stricter. |
| [`examples`][js-examples] | The first entry fills `example` when that is missing. |
Expand Down
53 changes: 48 additions & 5 deletions packages/downgrader/src/v3.1-to-v3.0.ts
Original file line number Diff line number Diff line change
Expand Up @@ -748,11 +748,11 @@ function mergeEnums(out: Record<string, unknown>, key: 'anyOf' | 'oneOf'): void
const branches = out[key] as unknown[]
const enums = branches.filter((branch) => !isNullBranch(branch, out))
const type = enumUnionType(enums)
if (
type === undefined ||
(out.type !== undefined && out.type !== type) ||
out.enum !== undefined
) {
if ((out.type !== undefined && out.type !== type) || out.enum !== undefined) {
return
}
if (type === undefined) {
mergeEnumsByType(out, key)
return
}
const values: unknown[] = []
Expand Down Expand Up @@ -786,6 +786,49 @@ function mergeEnums(out: Record<string, unknown>, key: 'anyOf' | 'oneOf'): void
}
}

// Zod, ArkType and Valibot write a union of literals of several types, or of
// literals and other types, as `'a' | 'b' | number`, with a branch for each
// literal, and utoipa so writes the unit variants of a Rust enum beside its
// other variants. The enums of each type become one, so `scalarOneOf` can then
// make a `oneOf` of a union of scalars.
function mergeEnumsByType(out: Record<string, unknown>, key: 'anyOf' | 'oneOf'): void {
const branches = out[key] as unknown[]
const merged = new Map<unknown, { enum: unknown[]; seen: Set<unknown> }>()
const result: unknown[] = []
for (const branch of branches) {
if (
!isRecord(branch) ||
typeof branch.type !== 'string' ||
!Array.isArray(branch.enum) ||
branch.nullable === true ||
!Object.keys(branch).every(
(name) => name === 'type' || name === 'enum' || repeats(out, branch, name),
)
) {
result.push(branch)
continue
}
let group = merged.get(branch.type)
if (group === undefined) {
group = { enum: [], seen: new Set() }
merged.set(branch.type, group)
result.push({ type: branch.type, enum: group.enum })
}
for (const value of branch.enum) {
const valueKey = jsonKey(value)
if (!group.seen.has(valueKey)) {
group.seen.add(valueKey)
group.enum.push(value)
} else if (key === 'oneOf') {
return
}
}
}
if (result.length < branches.length) {
out[key] = result
}
}

const ONE_OF_TYPES = new Set(['array', 'boolean', 'integer', 'number', 'string'])

// Zod writes a union of scalars as a `type` list, which becomes an `anyOf`,
Expand Down
Loading
Loading