Skip to content

Commit 81010aa

Browse files
dinwwwhclaude
andauthored
test(downgrader): cover mixed literal union edge cases, note boolean-ish flags with coercing validators (#136)
Three cases close the Stryker gaps in the enum merge of a literal union: a described branch stops the same-type merge, typeless multi-value enums stay as written, and a lone described typed enum stays as written. The "Validators that coerce" README line now says a query flag of "true", "false", "0", "1" or a boolean, as GrowthBook writes, rejects ?flag=true, since "true" matches the string and, coerced, the boolean of the oneOf. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018XRYDXpPjZLwYYLKH7PZWd
1 parent b10f7fa commit 81010aa

2 files changed

Lines changed: 23 additions & 1 deletion

File tree

‎packages/downgrader/README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -262,7 +262,7 @@ Accepts the default OAS dialect, which adds `discriminator`, `xml`, `externalDoc
262262
- datamodel-code-generator reads a required property with `nullable: true` as never null. Pass `--strict-nullable` to keep null.
263263
- aiopenapi3 fails to load a 3.0 document that holds the `"null"` branch: it takes `null` out of a nullable `enum` and builds an empty `Literal` from what is left. aiopenapi3 reads 3.1 too, which keeps `{ type: "null" }`.
264264
- oapi-codegen 2.8 rejects, and the OpenAPI Generator C# client breaks on, a string enum with a `format` such as `uuid`, which a `const` with a `format` becomes, as ArkType writes for `string.uuid`.
265-
- Validators that coerce a value to each branch's `type`, as Prism, express-openapi-validator and openapi-backend do for parameters, and Fastify by default for bodies too, match a value such as `1` or `"1"` to two branches of the `oneOf` a union of scalar types becomes, and reject it. Fastify by default also wraps a value in an array, so it rejects a single value, and an array of one item, against the `oneOf` a union of a scalar type and an array becomes, as for `string | string[]`. It also coerces in place, so a branch of a `oneOf` can change the value that later branches and the handler see. A number in Effect's `Schema.Number`, a `oneOf` of a number and a string enum, arrives as a string, and null in a nullable enum, a nullable `$ref` to an enum, or a nullable union of literals becomes a value such as `""`, which the `"null"` branch rejects where 3.1's `{ type: "null" }` turns it back into null. Prism, express-openapi-validator, openapi-backend and fastify-openapi-glue read 3.1 too, which keeps the `type` list or the `anyOf`.
265+
- Validators that coerce a value to each branch's `type`, as Prism, express-openapi-validator and openapi-backend do for parameters, and Fastify by default for bodies too, match a value such as `1` or `"1"` to two branches of the `oneOf` a union of scalar types becomes, and reject it. So a query flag that takes `"true"`, `"false"`, `"0"`, `"1"` or a boolean, as GrowthBook writes, rejects `?flag=true`, since `"true"` matches the string and, coerced, the boolean. Fastify by default also wraps a value in an array, so it rejects a single value, and an array of one item, against the `oneOf` a union of a scalar type and an array becomes, as for `string | string[]`. It also coerces in place, so a branch of a `oneOf` can change the value that later branches and the handler see. A number in Effect's `Schema.Number`, a `oneOf` of a number and a string enum, arrives as a string, and null in a nullable enum, a nullable `$ref` to an enum, or a nullable union of literals becomes a value such as `""`, which the `"null"` branch rejects where 3.1's `{ type: "null" }` turns it back into null. Prism, express-openapi-validator, openapi-backend and fastify-openapi-glue read 3.1 too, which keeps the `type` list or the `anyOf`.
266266
- Kiota reads the `"null"` branch's `type: "string"` as a real type, so array items that may be null, and a union of several types with null, come out as strings. Kiota reads 3.1 too, which keeps their types.
267267
- Speakeasy, like Kiota, reads the `"null"` branch's `type: "string"` as a string, so a nullable `$ref` or enum also takes any string, and a nullable union of scalars with a string becomes `any`, since it then holds two strings. It also fails on a recursive component, reached from another schema, with a property of `type: "null"` alone, such as Zod's `z.null()`. Speakeasy reads 3.1 too, which keeps their types.
268268
- Hey API's Zod and Valibot plugins read the `"null"` branch as any value, so a union with null, such as a nullable `$ref`, accepts every value. Hey API reads 3.1 too, which keeps the `"null"` type.

‎packages/downgrader/tests/v3.1-to-v3.0.test.ts‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1224,6 +1224,28 @@ describe('downgradeSchemaV31ToV30', () => {
12241224
{ title: 'C', type: 'string', enum: ['c'] },
12251225
],
12261226
})
1227+
// Enums of one type with another keyword among them, or with no `type`, stay.
1228+
expect(
1229+
downgradeSchemaV31ToV30({
1230+
anyOf: [{ const: 'a' }, { const: 'b', description: 'B' }, { const: 'c' }, { type: 'null' }],
1231+
}),
1232+
).toEqual({
1233+
anyOf: [
1234+
{ type: 'string', enum: ['a'] },
1235+
{ description: 'B', type: 'string', enum: ['b'] },
1236+
{ type: 'string', enum: ['c'] },
1237+
{ type: 'string', nullable: true, enum: [null] },
1238+
],
1239+
})
1240+
// A branch stays as written when no other enum shares its `type`.
1241+
const described: OpenAPIV3_1.SchemaObject = { type: 'string', enum: ['a'], description: 'D' }
1242+
expect(
1243+
downgradeSchemaV31ToV30({ description: 'D', anyOf: [described, { type: 'boolean' }] }),
1244+
).toEqual({ description: 'D', oneOf: [described, { type: 'boolean' }] })
1245+
const typeless: OpenAPIV3_1.SchemaObject = {
1246+
anyOf: [{ enum: ['a', 1] }, { enum: ['b', 2] }, { type: 'boolean' }],
1247+
}
1248+
expect(downgradeSchemaV31ToV30(typeless)).toEqual(typeless)
12271249
expect(
12281250
downgradeSchemaV31ToV30({ anyOf: [{ const: 'a' }, { const: 1 }, { const: 'a' }] }),
12291251
).toEqual({

0 commit comments

Comments
 (0)