From 34b92268f5146955f2c51b7648380e2b925ea184 Mon Sep 17 00:00:00 2001 From: Dinh Le Date: Sun, 27 Sep 2026 09:50:44 +0700 Subject: [PATCH 1/6] fix(downgrader): keep binary content in 3.1 to 3.0 Raw binary (`contentMediaType` without `contentEncoding`) now becomes `type: string` + `format: binary` for every media type, not just `application/octet-stream`, and `contentEncoding: base64` now also gets `type: string` beside `format: byte`, following the spec's binary migration table. Before, `contentMediaType: image/png` became `{}`. --- packages/downgrader/README.md | 36 +++++++-------- packages/downgrader/src/v3.1-to-v3.0.test.ts | 46 +++++++++++++------- packages/downgrader/src/v3.1-to-v3.0.ts | 28 +++++++++--- packages/downgrader/tests/e2e.test.ts | 30 +++++++++++++ 4 files changed, 101 insertions(+), 39 deletions(-) diff --git a/packages/downgrader/README.md b/packages/downgrader/README.md index 330c9fc..7e3d9fe 100644 --- a/packages/downgrader/README.md +++ b/packages/downgrader/README.md @@ -111,24 +111,24 @@ Removed, with no 3.0 equivalent: Schema Objects: -| 3.1 construct | 3.0 result | -| ---------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `true` / `false` boolean schemas | `{}` / `{ not: {} }` | -| `$ref` with sibling keywords | siblings kept, `$ref` moved into `allOf` | -| `type: ["T", "null"]` | `type: "T"` plus `nullable: true` | -| `type` with several non-null entries | `anyOf` of single-type schemas, each `nullable` when `null` was listed. A sibling `items` moves into the `array` variant | -| `type: "null"` | `enum: [null]`, since 3.0 ignores `nullable` without a `type`. A sibling `enum` or `const` is intersected with the null type: an `enum` containing `null` collapses to `[null]`, and one excluding it yields `not: {}`, since the source accepted no value | -| `const` | single-value `enum` | -| numeric `exclusiveMinimum` / `exclusiveMaximum` | `minimum` / `maximum` plus the boolean flag; a tighter existing bound wins | -| `examples` | first entry becomes `example` when none exists | -| `contentEncoding: base64` | `format: byte` when no `format` exists | -| `contentMediaType: application/octet-stream` without `contentEncoding` | `format: binary` when no `format` exists | -| `type: "array"` without `items` | `items: {}` added (required in 3.0) | -| `enum: []` | removed (3.0 requires a non-empty `enum`) | -| `required: []` / duplicate `required` entries | removed / deduplicated (3.0 requires a non-empty, unique `required`) | -| XML `nodeType`, carried over from a 3.2 chain | `attribute: true` / `wrapped: true` where expressible, then removed (3.0 forbids unknown XML Object fields) | - -Removed, with no 3.0 equivalent: `$schema`, `$id`, `$defs`, `$anchor`, `$dynamicRef`, `$dynamicAnchor`, `$vocabulary`, `$comment`, `if` / `then` / `else`, `dependentSchemas`, `dependentRequired`, `prefixItems` (with its trailing `items`), `contains`, `minContains`, `maxContains`, `patternProperties` (with its sibling `additionalProperties`, whose meaning would otherwise tighten onto the pattern-matched keys), `propertyNames`, `unevaluatedItems`, `unevaluatedProperties`, and `contentSchema`. In positive schema positions dropping these only loosens validation, the safe direction for a downgrade. +| 3.1 construct | 3.0 result | +| ----------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `true` / `false` boolean schemas | `{}` / `{ not: {} }` | +| `$ref` with sibling keywords | siblings kept, `$ref` moved into `allOf` | +| `type: ["T", "null"]` | `type: "T"` plus `nullable: true` | +| `type` with several non-null entries | `anyOf` of single-type schemas, each `nullable` when `null` was listed. A sibling `items` moves into the `array` variant | +| `type: "null"` | `enum: [null]`, since 3.0 ignores `nullable` without a `type`. A sibling `enum` or `const` is intersected with the null type: an `enum` containing `null` collapses to `[null]`, and one excluding it yields `not: {}`, since the source accepted no value | +| `const` | single-value `enum` | +| numeric `exclusiveMinimum` / `exclusiveMaximum` | `minimum` / `maximum` plus the boolean flag; a tighter existing bound wins | +| `examples` | first entry becomes `example` when none exists | +| `contentEncoding: base64` | `format: byte`, plus `type: string` when `type` is missing. An existing `format` wins, and a `type` excluding `string` gets neither | +| `contentMediaType` without `contentEncoding` | as above, with `format: binary` | +| `type: "array"` without `items` | `items: {}` added (required in 3.0) | +| `enum: []` | removed (3.0 requires a non-empty `enum`) | +| `required: []` / duplicate `required` entries | removed / deduplicated (3.0 requires a non-empty, unique `required`) | +| XML `nodeType`, carried over from a 3.2 chain | `attribute: true` / `wrapped: true` where expressible, then removed (3.0 forbids unknown XML Object fields) | + +Removed, with no 3.0 equivalent: `$schema`, `$id`, `$defs`, `$anchor`, `$dynamicRef`, `$dynamicAnchor`, `$vocabulary`, `$comment`, `if` / `then` / `else`, `dependentSchemas`, `dependentRequired`, `prefixItems` (with its trailing `items`), `contains`, `minContains`, `maxContains`, `patternProperties` (with its sibling `additionalProperties`, whose meaning would otherwise tighten onto the pattern-matched keys), `propertyNames`, `unevaluatedItems`, `unevaluatedProperties`, `contentSchema`, and a non-`base64` `contentEncoding` (`base64url` included) with its `contentMediaType`. In positive schema positions dropping these only loosens validation, the safe direction for a downgrade. Known limitations: diff --git a/packages/downgrader/src/v3.1-to-v3.0.test.ts b/packages/downgrader/src/v3.1-to-v3.0.test.ts index 903c7fd..f22912e 100644 --- a/packages/downgrader/src/v3.1-to-v3.0.test.ts +++ b/packages/downgrader/src/v3.1-to-v3.0.test.ts @@ -1105,32 +1105,48 @@ describe('downgradeSchemaV31ToV30', () => { describe('content keywords', () => { it.each([ [ - 'converts contentEncoding base64 into format byte', + 'converts encoded binary into type string with format byte', + { contentEncoding: 'base64', contentMediaType: 'image/png', type: 'string' }, + { format: 'byte', type: 'string' }, + ], + [ + 'adds type string to contentEncoding base64 without a type', { contentEncoding: 'base64' }, - { format: 'byte' }, + { format: 'byte', type: 'string' }, + ], + [ + 'converts raw binary into type string with format binary', + { contentMediaType: 'image/png' }, + { format: 'binary', type: 'string' }, + ], + [ + 'keeps nullable on binary strings', + { contentMediaType: 'image/png', type: ['string', 'null'] }, + { format: 'binary', nullable: true, type: 'string' }, + ], + [ + 'keeps format beside a multi-type anyOf that includes string', + { contentMediaType: 'image/png', type: ['string', 'integer'] }, + { anyOf: [{ type: 'string' }, { type: 'integer' }], format: 'binary' }, ], [ 'keeps an existing format over contentEncoding', { contentEncoding: 'base64', format: 'custom' }, - { format: 'custom' }, + { format: 'custom', type: 'string' }, ], - ['drops other content encodings', { contentEncoding: 'gzip' }, {}], [ - 'converts contentMediaType application/octet-stream into format binary', - { contentMediaType: 'application/octet-stream' }, - { format: 'binary' }, + 'drops content keywords on non-string types', + { contentMediaType: 'image/png', type: 'object' }, + { type: 'object' }, ], [ - 'does not emit format binary when a contentEncoding is present', - { - contentEncoding: 'gzip', - contentMediaType: 'application/octet-stream', - }, - {}, + 'drops base64url, which format byte does not accept', + { contentEncoding: 'base64url', contentMediaType: 'image/png', type: 'string' }, + { type: 'string' }, ], [ - 'drops other content media types', - { contentMediaType: 'image/png' }, + 'drops non-string content media types', + { contentMediaType: 42 }, {}, ], ['drops contentSchema', { contentSchema: { type: 'string' } }, {}], diff --git a/packages/downgrader/src/v3.1-to-v3.0.ts b/packages/downgrader/src/v3.1-to-v3.0.ts index e8ef1a4..2aef2aa 100644 --- a/packages/downgrader/src/v3.1-to-v3.0.ts +++ b/packages/downgrader/src/v3.1-to-v3.0.ts @@ -80,6 +80,10 @@ function applyTypes(types: string[], schema: Record, out: Recor } } +function hasType(type: unknown, name: string): boolean { + return type === name || (Array.isArray(type) && type.includes(name)) +} + function convertType(schema: Record, out: Record): void { const { type } = schema if (type === undefined) { @@ -123,15 +127,27 @@ function convertExclusiveBounds(schema: Record, out: Record): string | undefined { + if (schema.contentEncoding === 'base64') { + return 'byte' + } + if (schema.contentEncoding === undefined && typeof schema.contentMediaType === 'string') { + return 'binary' + } + return undefined +} + function convertContentKeywords(schema: Record, out: Record): void { - if (out.format !== undefined) { + const format = getContentFormat(schema) + const { type } = schema + if (format === undefined || (type !== undefined && !hasType(type, 'string'))) { return } - if (schema.contentEncoding === 'base64') { - out.format = 'byte' + if (type === undefined) { + out.type = 'string' } - else if (schema.contentEncoding === undefined && schema.contentMediaType === 'application/octet-stream') { - out.format = 'binary' + if (out.format === undefined) { + out.format = format } } @@ -140,7 +156,7 @@ function convertXml(value: unknown, schemaType: unknown): unknown { if (xml.nodeType === 'attribute') { out.attribute = true } - else if (xml.nodeType === 'element' && (schemaType === 'array' || (Array.isArray(schemaType) && schemaType.includes('array')))) { + else if (xml.nodeType === 'element' && hasType(schemaType, 'array')) { out.wrapped = true } return out diff --git a/packages/downgrader/tests/e2e.test.ts b/packages/downgrader/tests/e2e.test.ts index e9f38f2..c123272 100644 --- a/packages/downgrader/tests/e2e.test.ts +++ b/packages/downgrader/tests/e2e.test.ts @@ -138,6 +138,36 @@ describe('3.1 example documents downgraded to 3.0', () => { await expectValidAs(converted, '3.0') expect(doc).toEqual(before) }) + + it('converts raw and encoded binary schemas to the 3.0 binary and byte formats', async () => { + const doc: OpenAPIV3_1.OpenAPIObject = { + info: { title: 'Uploads', version: '1.0.0' }, + openapi: '3.1.0', + paths: { + '/avatar': { + put: { + requestBody: { + content: { + 'image/png': { schema: { contentMediaType: 'image/png' } }, + 'text/plain': { schema: { contentEncoding: 'base64', contentMediaType: 'image/png', type: 'string' } }, + }, + }, + responses: { 204: { description: 'saved' } }, + }, + }, + }, + } + const before = structuredClone(doc) + const converted = downgradeSpecV31ToV30(doc) + expect(converted.paths['/avatar']?.put?.requestBody).toEqual({ + content: { + 'image/png': { schema: { format: 'binary', type: 'string' } }, + 'text/plain': { schema: { format: 'byte', type: 'string' } }, + }, + }) + await expectValidAs(converted, '3.0') + expect(doc).toEqual(before) + }) }) describe('3.2 example documents downgraded to 3.1 and chained to 3.0', () => { From 0349ebb968385468fd491705c94a011400cfe1ab Mon Sep 17 00:00:00 2001 From: Dinh Le Date: Sun, 27 Sep 2026 10:07:23 +0700 Subject: [PATCH 2/6] fix(downgrader): skip type string beside non-string const or enum Adding type: string next to a non-string const or enum made the 3.0 schema accept nothing, while the 3.1 source accepted those values. format is still added, since it only applies to strings. --- packages/downgrader/README.md | 2 +- packages/downgrader/src/v3.1-to-v3.0.test.ts | 15 +++++++++++++++ packages/downgrader/src/v3.1-to-v3.0.ts | 7 ++++++- 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/packages/downgrader/README.md b/packages/downgrader/README.md index 7e3d9fe..baccd09 100644 --- a/packages/downgrader/README.md +++ b/packages/downgrader/README.md @@ -121,7 +121,7 @@ Schema Objects: | `const` | single-value `enum` | | numeric `exclusiveMinimum` / `exclusiveMaximum` | `minimum` / `maximum` plus the boolean flag; a tighter existing bound wins | | `examples` | first entry becomes `example` when none exists | -| `contentEncoding: base64` | `format: byte`, plus `type: string` when `type` is missing. An existing `format` wins, and a `type` excluding `string` gets neither | +| `contentEncoding: base64` | `format: byte`, plus `type: string` when `type` is missing and every `const` / `enum` value is a string. An existing `format` wins, and a `type` excluding `string` gets neither | | `contentMediaType` without `contentEncoding` | as above, with `format: binary` | | `type: "array"` without `items` | `items: {}` added (required in 3.0) | | `enum: []` | removed (3.0 requires a non-empty `enum`) | diff --git a/packages/downgrader/src/v3.1-to-v3.0.test.ts b/packages/downgrader/src/v3.1-to-v3.0.test.ts index f22912e..38a1d36 100644 --- a/packages/downgrader/src/v3.1-to-v3.0.test.ts +++ b/packages/downgrader/src/v3.1-to-v3.0.test.ts @@ -1134,6 +1134,21 @@ describe('downgradeSchemaV31ToV30', () => { { contentEncoding: 'base64', format: 'custom' }, { format: 'custom', type: 'string' }, ], + [ + 'adds type string beside an all-string enum', + { contentMediaType: 'text/plain', enum: ['a', 'b'] }, + { enum: ['a', 'b'], format: 'binary', type: 'string' }, + ], + [ + 'does not add type string beside a non-string const', + { const: 42, contentEncoding: 'base64' }, + { enum: [42], format: 'byte' }, + ], + [ + 'does not add type string beside an enum with non-string values', + { contentMediaType: 'image/png', enum: ['x', null] }, + { enum: ['x', null], format: 'binary' }, + ], [ 'drops content keywords on non-string types', { contentMediaType: 'image/png', type: 'object' }, diff --git a/packages/downgrader/src/v3.1-to-v3.0.ts b/packages/downgrader/src/v3.1-to-v3.0.ts index 2aef2aa..f719151 100644 --- a/packages/downgrader/src/v3.1-to-v3.0.ts +++ b/packages/downgrader/src/v3.1-to-v3.0.ts @@ -137,13 +137,18 @@ function getContentFormat(schema: Record): string | undefined { return undefined } +function hasNonStringValue(schema: Record): boolean { + return ('const' in schema && typeof schema.const !== 'string') + || (Array.isArray(schema.enum) && schema.enum.some(item => typeof item !== 'string')) +} + function convertContentKeywords(schema: Record, out: Record): void { const format = getContentFormat(schema) const { type } = schema if (format === undefined || (type !== undefined && !hasType(type, 'string'))) { return } - if (type === undefined) { + if (type === undefined && !hasNonStringValue(schema)) { out.type = 'string' } if (out.format === undefined) { From fac03ba0900b71ba75cb0056b3871f14f840f8f0 Mon Sep 17 00:00:00 2001 From: Dinh Le Date: Sun, 27 Sep 2026 10:12:59 +0700 Subject: [PATCH 3/6] chore(downgrader): trim redundant binary test and README wording --- packages/downgrader/README.md | 2 +- packages/downgrader/src/v3.1-to-v3.0.test.ts | 5 ----- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/packages/downgrader/README.md b/packages/downgrader/README.md index baccd09..16e9288 100644 --- a/packages/downgrader/README.md +++ b/packages/downgrader/README.md @@ -121,7 +121,7 @@ Schema Objects: | `const` | single-value `enum` | | numeric `exclusiveMinimum` / `exclusiveMaximum` | `minimum` / `maximum` plus the boolean flag; a tighter existing bound wins | | `examples` | first entry becomes `example` when none exists | -| `contentEncoding: base64` | `format: byte`, plus `type: string` when `type` is missing and every `const` / `enum` value is a string. An existing `format` wins, and a `type` excluding `string` gets neither | +| `contentEncoding: base64` | `format: byte` unless `format` exists, and `type: string` when `type` is missing and `const` / `enum` hold only strings. Skipped when `type` excludes `string` | | `contentMediaType` without `contentEncoding` | as above, with `format: binary` | | `type: "array"` without `items` | `items: {}` added (required in 3.0) | | `enum: []` | removed (3.0 requires a non-empty `enum`) | diff --git a/packages/downgrader/src/v3.1-to-v3.0.test.ts b/packages/downgrader/src/v3.1-to-v3.0.test.ts index 38a1d36..8a2a374 100644 --- a/packages/downgrader/src/v3.1-to-v3.0.test.ts +++ b/packages/downgrader/src/v3.1-to-v3.0.test.ts @@ -1109,11 +1109,6 @@ describe('downgradeSchemaV31ToV30', () => { { contentEncoding: 'base64', contentMediaType: 'image/png', type: 'string' }, { format: 'byte', type: 'string' }, ], - [ - 'adds type string to contentEncoding base64 without a type', - { contentEncoding: 'base64' }, - { format: 'byte', type: 'string' }, - ], [ 'converts raw binary into type string with format binary', { contentMediaType: 'image/png' }, From 664d2858866623df0149db72a50f11dfa0d2f209 Mon Sep 17 00:00:00 2001 From: Dinh Le Date: Sun, 27 Sep 2026 10:21:38 +0700 Subject: [PATCH 4/6] fix(downgrader): skip type string beside $ref and composition keywords $ref, allOf, anyOf, oneOf and not already limit the value type, so adding type: string beside them could make the 3.0 schema accept nothing or drop values the source accepted, such as null in a nullable anyOf. format is still added. --- packages/downgrader/README.md | 2 +- packages/downgrader/src/v3.1-to-v3.0.test.ts | 15 +++++++++++++++ packages/downgrader/src/v3.1-to-v3.0.ts | 7 ++++--- 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/packages/downgrader/README.md b/packages/downgrader/README.md index 16e9288..08b593e 100644 --- a/packages/downgrader/README.md +++ b/packages/downgrader/README.md @@ -121,7 +121,7 @@ Schema Objects: | `const` | single-value `enum` | | numeric `exclusiveMinimum` / `exclusiveMaximum` | `minimum` / `maximum` plus the boolean flag; a tighter existing bound wins | | `examples` | first entry becomes `example` when none exists | -| `contentEncoding: base64` | `format: byte` unless `format` exists, and `type: string` when `type` is missing and `const` / `enum` hold only strings. Skipped when `type` excludes `string` | +| `contentEncoding: base64` | `format: byte` unless `format` exists, and `type: string` unless `type`, `$ref`, `allOf` / `anyOf` / `oneOf` / `not`, or a non-string `const` / `enum` value already limits the type. Skipped when `type` excludes `string` | | `contentMediaType` without `contentEncoding` | as above, with `format: binary` | | `type: "array"` without `items` | `items: {}` added (required in 3.0) | | `enum: []` | removed (3.0 requires a non-empty `enum`) | diff --git a/packages/downgrader/src/v3.1-to-v3.0.test.ts b/packages/downgrader/src/v3.1-to-v3.0.test.ts index 8a2a374..ee05066 100644 --- a/packages/downgrader/src/v3.1-to-v3.0.test.ts +++ b/packages/downgrader/src/v3.1-to-v3.0.test.ts @@ -1144,6 +1144,21 @@ describe('downgradeSchemaV31ToV30', () => { { contentMediaType: 'image/png', enum: ['x', null] }, { enum: ['x', null], format: 'binary' }, ], + [ + 'does not add type string beside allOf', + { allOf: [{ type: 'object' }], contentMediaType: 'image/png' }, + { allOf: [{ type: 'object' }], format: 'binary' }, + ], + [ + 'does not add type string beside a nullable anyOf', + { anyOf: [{ type: 'string' }, { type: 'null' }], contentEncoding: 'base64' }, + { anyOf: [{ type: 'string' }, { enum: [null] }], format: 'byte' }, + ], + [ + 'does not add type string beside $ref', + { $ref: '#/components/schemas/Image', contentMediaType: 'image/png' }, + { allOf: [{ $ref: '#/components/schemas/Image' }], format: 'binary' }, + ], [ 'drops content keywords on non-string types', { contentMediaType: 'image/png', type: 'object' }, diff --git a/packages/downgrader/src/v3.1-to-v3.0.ts b/packages/downgrader/src/v3.1-to-v3.0.ts index f719151..6b71d2d 100644 --- a/packages/downgrader/src/v3.1-to-v3.0.ts +++ b/packages/downgrader/src/v3.1-to-v3.0.ts @@ -137,8 +137,9 @@ function getContentFormat(schema: Record): string | undefined { return undefined } -function hasNonStringValue(schema: Record): boolean { - return ('const' in schema && typeof schema.const !== 'string') +function hasTypeConstraint(schema: Record): boolean { + return '$ref' in schema || 'allOf' in schema || 'anyOf' in schema || 'not' in schema || 'oneOf' in schema + || ('const' in schema && typeof schema.const !== 'string') || (Array.isArray(schema.enum) && schema.enum.some(item => typeof item !== 'string')) } @@ -148,7 +149,7 @@ function convertContentKeywords(schema: Record, out: Record Date: Sun, 27 Sep 2026 10:26:45 +0700 Subject: [PATCH 5/6] refactor(downgrader): list type-limiting keywords in a constant --- packages/downgrader/README.md | 2 +- packages/downgrader/src/v3.1-to-v3.0.ts | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/downgrader/README.md b/packages/downgrader/README.md index 08b593e..302348f 100644 --- a/packages/downgrader/README.md +++ b/packages/downgrader/README.md @@ -121,7 +121,7 @@ Schema Objects: | `const` | single-value `enum` | | numeric `exclusiveMinimum` / `exclusiveMaximum` | `minimum` / `maximum` plus the boolean flag; a tighter existing bound wins | | `examples` | first entry becomes `example` when none exists | -| `contentEncoding: base64` | `format: byte` unless `format` exists, and `type: string` unless `type`, `$ref`, `allOf` / `anyOf` / `oneOf` / `not`, or a non-string `const` / `enum` value already limits the type. Skipped when `type` excludes `string` | +| `contentEncoding: base64` | `format: byte` unless `format` exists, plus `type: string` when no `type`, `$ref`, composition keyword, or non-string `const` / `enum` limits the type. Skipped when `type` excludes `string` | | `contentMediaType` without `contentEncoding` | as above, with `format: binary` | | `type: "array"` without `items` | `items: {}` added (required in 3.0) | | `enum: []` | removed (3.0 requires a non-empty `enum`) | diff --git a/packages/downgrader/src/v3.1-to-v3.0.ts b/packages/downgrader/src/v3.1-to-v3.0.ts index 6b71d2d..e2d2285 100644 --- a/packages/downgrader/src/v3.1-to-v3.0.ts +++ b/packages/downgrader/src/v3.1-to-v3.0.ts @@ -137,8 +137,10 @@ function getContentFormat(schema: Record): string | undefined { return undefined } +const TYPE_CONSTRAINT_KEYWORDS = ['$ref', 'allOf', 'anyOf', 'not', 'oneOf'] + function hasTypeConstraint(schema: Record): boolean { - return '$ref' in schema || 'allOf' in schema || 'anyOf' in schema || 'not' in schema || 'oneOf' in schema + return TYPE_CONSTRAINT_KEYWORDS.some(keyword => keyword in schema) || ('const' in schema && typeof schema.const !== 'string') || (Array.isArray(schema.enum) && schema.enum.some(item => typeof item !== 'string')) } From 33b3d5de873d14beed7330bdc801756b72631553 Mon Sep 17 00:00:00 2001 From: Dinh Le Date: Sun, 27 Sep 2026 10:31:21 +0700 Subject: [PATCH 6/6] refactor(downgrader): add type string whenever type is missing Drop the guard that skipped type: string beside $ref, composition keywords, and non-string const or enum. It only protected self-contradictory schemas (binary content that is also a number or an object) that generators do not emit, so the simpler rule wins. --- packages/downgrader/README.md | 2 +- packages/downgrader/src/v3.1-to-v3.0.test.ts | 30 -------------------- packages/downgrader/src/v3.1-to-v3.0.ts | 10 +------ 3 files changed, 2 insertions(+), 40 deletions(-) diff --git a/packages/downgrader/README.md b/packages/downgrader/README.md index 302348f..b4c64b1 100644 --- a/packages/downgrader/README.md +++ b/packages/downgrader/README.md @@ -121,7 +121,7 @@ Schema Objects: | `const` | single-value `enum` | | numeric `exclusiveMinimum` / `exclusiveMaximum` | `minimum` / `maximum` plus the boolean flag; a tighter existing bound wins | | `examples` | first entry becomes `example` when none exists | -| `contentEncoding: base64` | `format: byte` unless `format` exists, plus `type: string` when no `type`, `$ref`, composition keyword, or non-string `const` / `enum` limits the type. Skipped when `type` excludes `string` | +| `contentEncoding: base64` | `format: byte` unless `format` exists, plus `type: string` when `type` is missing. Skipped when `type` excludes `string` | | `contentMediaType` without `contentEncoding` | as above, with `format: binary` | | `type: "array"` without `items` | `items: {}` added (required in 3.0) | | `enum: []` | removed (3.0 requires a non-empty `enum`) | diff --git a/packages/downgrader/src/v3.1-to-v3.0.test.ts b/packages/downgrader/src/v3.1-to-v3.0.test.ts index ee05066..3755941 100644 --- a/packages/downgrader/src/v3.1-to-v3.0.test.ts +++ b/packages/downgrader/src/v3.1-to-v3.0.test.ts @@ -1129,36 +1129,6 @@ describe('downgradeSchemaV31ToV30', () => { { contentEncoding: 'base64', format: 'custom' }, { format: 'custom', type: 'string' }, ], - [ - 'adds type string beside an all-string enum', - { contentMediaType: 'text/plain', enum: ['a', 'b'] }, - { enum: ['a', 'b'], format: 'binary', type: 'string' }, - ], - [ - 'does not add type string beside a non-string const', - { const: 42, contentEncoding: 'base64' }, - { enum: [42], format: 'byte' }, - ], - [ - 'does not add type string beside an enum with non-string values', - { contentMediaType: 'image/png', enum: ['x', null] }, - { enum: ['x', null], format: 'binary' }, - ], - [ - 'does not add type string beside allOf', - { allOf: [{ type: 'object' }], contentMediaType: 'image/png' }, - { allOf: [{ type: 'object' }], format: 'binary' }, - ], - [ - 'does not add type string beside a nullable anyOf', - { anyOf: [{ type: 'string' }, { type: 'null' }], contentEncoding: 'base64' }, - { anyOf: [{ type: 'string' }, { enum: [null] }], format: 'byte' }, - ], - [ - 'does not add type string beside $ref', - { $ref: '#/components/schemas/Image', contentMediaType: 'image/png' }, - { allOf: [{ $ref: '#/components/schemas/Image' }], format: 'binary' }, - ], [ 'drops content keywords on non-string types', { contentMediaType: 'image/png', type: 'object' }, diff --git a/packages/downgrader/src/v3.1-to-v3.0.ts b/packages/downgrader/src/v3.1-to-v3.0.ts index e2d2285..2aef2aa 100644 --- a/packages/downgrader/src/v3.1-to-v3.0.ts +++ b/packages/downgrader/src/v3.1-to-v3.0.ts @@ -137,21 +137,13 @@ function getContentFormat(schema: Record): string | undefined { return undefined } -const TYPE_CONSTRAINT_KEYWORDS = ['$ref', 'allOf', 'anyOf', 'not', 'oneOf'] - -function hasTypeConstraint(schema: Record): boolean { - return TYPE_CONSTRAINT_KEYWORDS.some(keyword => keyword in schema) - || ('const' in schema && typeof schema.const !== 'string') - || (Array.isArray(schema.enum) && schema.enum.some(item => typeof item !== 'string')) -} - function convertContentKeywords(schema: Record, out: Record): void { const format = getContentFormat(schema) const { type } = schema if (format === undefined || (type !== undefined && !hasType(type, 'string'))) { return } - if (type === undefined && !hasTypeConstraint(schema)) { + if (type === undefined) { out.type = 'string' } if (out.format === undefined) {