diff --git a/packages/downgrader/README.md b/packages/downgrader/README.md index e1930a1..f3ae98b 100644 --- a/packages/downgrader/README.md +++ b/packages/downgrader/README.md @@ -262,7 +262,7 @@ Accepts the default OAS dialect, which adds `discriminator`, `xml`, `externalDoc - datamodel-code-generator reads a required property with `nullable: true` as never null. Pass `--strict-nullable` to keep null. - 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" }`. - 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`. -- 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`. +- 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`. - 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. - 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. - 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. diff --git a/packages/downgrader/tests/v3.1-to-v3.0.test.ts b/packages/downgrader/tests/v3.1-to-v3.0.test.ts index c204551..91ec5ea 100644 --- a/packages/downgrader/tests/v3.1-to-v3.0.test.ts +++ b/packages/downgrader/tests/v3.1-to-v3.0.test.ts @@ -1224,6 +1224,28 @@ describe('downgradeSchemaV31ToV30', () => { { title: 'C', type: 'string', enum: ['c'] }, ], }) + // Enums of one type with another keyword among them, or with no `type`, stay. + expect( + downgradeSchemaV31ToV30({ + anyOf: [{ const: 'a' }, { const: 'b', description: 'B' }, { const: 'c' }, { type: 'null' }], + }), + ).toEqual({ + anyOf: [ + { type: 'string', enum: ['a'] }, + { description: 'B', type: 'string', enum: ['b'] }, + { type: 'string', enum: ['c'] }, + { type: 'string', nullable: true, enum: [null] }, + ], + }) + // A branch stays as written when no other enum shares its `type`. + const described: OpenAPIV3_1.SchemaObject = { type: 'string', enum: ['a'], description: 'D' } + expect( + downgradeSchemaV31ToV30({ description: 'D', anyOf: [described, { type: 'boolean' }] }), + ).toEqual({ description: 'D', oneOf: [described, { type: 'boolean' }] }) + const typeless: OpenAPIV3_1.SchemaObject = { + anyOf: [{ enum: ['a', 1] }, { enum: ['b', 2] }, { type: 'boolean' }], + } + expect(downgradeSchemaV31ToV30(typeless)).toEqual(typeless) expect( downgradeSchemaV31ToV30({ anyOf: [{ const: 'a' }, { const: 1 }, { const: 'a' }] }), ).toEqual({