diff --git a/packages/downgrader/README.md b/packages/downgrader/README.md index 1f618a0..a81b953 100644 --- a/packages/downgrader/README.md +++ b/packages/downgrader/README.md @@ -190,7 +190,7 @@ Accepts the default OAS dialect, which adds `discriminator`, `xml`, `externalDoc | A [`$ref`][js-ref] with siblings | `allOf: [{ $ref }]` beside the siblings, which 3.0 would ignore. | | [`$defs`][js-defs], and `definitions`, its draft-07 name | Removed. Each `$ref` into it is replaced by its converted target, where a reference back into a target being inlined becomes `{}`. | | Boolean subschemas | `{}` for `true` and `{ not: {} }` for `false`, except in `additionalProperties`, which allows booleans. | -| [`patternProperties`][js-pattern-properties] | Removed, with the `additionalProperties` beside it, which would reject the properties it allowed. | +| [`patternProperties`][js-pattern-properties] | Removed. `additionalProperties` becomes `true`, as a schema or `false` there would reject the properties `patternProperties` allowed, and code generators read a missing one as closed. | | [`$schema`][js-schema], [`$vocabulary`][js-vocabulary], [`$id`][js-id], [`$anchor`][js-anchor], [`$dynamicRef`, `$dynamicAnchor`][js-dynamic], [`$comment`][js-comment], and [`contentSchema`][js-content-schema] | Removed. | | [`if`][js-if], [`then`][js-then], [`else`][js-else], [`dependentSchemas`][js-dependent-schemas], and [`dependentRequired`][js-dependent-required] | Removed. | | [`contains`][js-contains], [`minContains`][js-min-contains], [`maxContains`][js-max-contains], and [`propertyNames`][js-property-names] | Removed. | @@ -246,7 +246,7 @@ Accepts the default OAS dialect, which adds `discriminator`, `xml`, `externalDoc - `$ref`s into a keyword 3.0 removes, such as `prefixItems/0` or `propertyNames`, or into a `oneOf` that becomes `anyOf`, are left as written and dangle. - Recursion through `$defs` is cut to `{}` after one level. Where several `$ref`s enter the same cycle, the first copy made is reused, so a later one can be cut sooner. - Older drafts are not supported. `definitions` is read as `$defs`, but other keywords that only draft-07 or 2019-09 define, such as `dependencies` and `additionalItems`, become extensions unconverted, so `$ref`s into them dangle, and array-form `items` passes through. Convert such schemas to 2020-12 first. -- `patternProperties` that matches every name, as TypeBox emits for a record, is removed with its value schema rather than turned into `additionalProperties`. +- `patternProperties` that matches every name, as TypeBox emits for a record, gives `additionalProperties: true`, so its value schema is lost. - An untyped schema that gains a `format` from `contentEncoding` or `contentMediaType` also gains `type: "string"`, as 3.0 binary strings need, so it rejects non-string values it accepted. 3.0 tools find file uploads only by `type: "string"` with `format: "binary"`. - OpenAPI Generator merges the `"null"` branch beside a `$ref` to a string enum into a new enum, so the reference is lost. Pass `--openapi-normalizer SIMPLIFY_ONEOF_ANYOF_ENUM=false` to keep it. - datamodel-code-generator reads a required property with `nullable: true` as never null. Pass `--strict-nullable` to keep null. diff --git a/packages/downgrader/src/v3.1-to-v3.0.ts b/packages/downgrader/src/v3.1-to-v3.0.ts index 0b8ff35..d25e2e2 100644 --- a/packages/downgrader/src/v3.1-to-v3.0.ts +++ b/packages/downgrader/src/v3.1-to-v3.0.ts @@ -648,6 +648,12 @@ function finishSchema( if (Array.isArray(schema.examples) && schema.examples.length > 0 && out.example === undefined) { out.example = clone(schema.examples[0]) } + // 3.0 lacks `patternProperties`, and `additionalProperties` beside it is + // dropped, as it would reject the properties `patternProperties` allowed. + // Code generators read a missing one as allowing no other properties. + if (schema.patternProperties !== undefined) { + out.additionalProperties = true + } const format = binaryFormat(schema) if (format !== undefined && (schema.type === undefined || hasType(schema.type, 'string'))) { // A `format` that only repeats the encoding, as Zod writes, gives way. 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 9b9d80a..ead90d9 100644 --- a/packages/downgrader/tests/v3.1-to-v3.0.test.ts +++ b/packages/downgrader/tests/v3.1-to-v3.0.test.ts @@ -1100,14 +1100,17 @@ describe('downgradeSchemaV31ToV30', () => { }) }) - it('drops additionalProperties beside patternProperties, which it would contradict', () => { + it('makes additionalProperties beside patternProperties true, which allows what patternProperties did', () => { + const patternProperties = { '^x-': { type: 'string' } } as const + // As Zod writes a loose record, and Effect a record with number keys. + for (const additionalProperties of [undefined, true, false, { type: 'number' }] as const) { + expect( + downgradeSchemaV31ToV30({ type: 'object', patternProperties, additionalProperties }), + ).toEqual({ type: 'object', additionalProperties: true }) + } expect( - downgradeSchemaV31ToV30({ - type: 'object', - patternProperties: { '^x-': { type: 'string' } }, - additionalProperties: false, - }), - ).toEqual({ type: 'object' }) + downgradeSchemaV31ToV30({ properties: { a: { type: 'string' } }, patternProperties }), + ).toEqual({ properties: { a: { type: 'string' } }, additionalProperties: true }) }) describe('a schema that lost a restriction', () => {