diff --git a/packages/downgrader/README.md b/packages/downgrader/README.md index 9869d34..8da72e9 100644 --- a/packages/downgrader/README.md +++ b/packages/downgrader/README.md @@ -111,22 +111,22 @@ 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 | -| `type: "null"` | `nullable: true` plus `enum: [null]`. 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`, plus `nullable: true` when the value is `null` | -| 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) | +| 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 | +| `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. 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 b7f7462..563f5b0 100644 --- a/packages/downgrader/src/v3.1-to-v3.0.test.ts +++ b/packages/downgrader/src/v3.1-to-v3.0.test.ts @@ -838,39 +838,44 @@ describe('downgradeSchemaV31ToV30', () => { { nullable: true, type: 'string' }, ], [ - 'converts a null-only type array into nullable plus a null enum', + 'converts a null-only type array into a null enum', { type: ['null'] }, - { enum: [null], nullable: true }, + { enum: [null] }, ], [ - 'converts a null-only type string into nullable plus a null enum', + 'converts a null-only type string into a null enum', { type: 'null' }, - { enum: [null], nullable: true }, + { enum: [null] }, ], [ 'intersects an existing enum with a null-only type', { enum: ['a', null], type: ['null'] }, - { enum: [null], nullable: true }, + { enum: [null] }, ], [ 'matches nothing when the enum of a null-only type excludes null', { enum: ['a'], type: ['null'] }, - { enum: ['a'], not: {}, nullable: true }, + { enum: ['a'], not: {} }, ], [ 'clones a malformed enum of a null-only type through', { enum: 'junk', type: ['null'] }, - { enum: 'junk', nullable: true }, + { enum: 'junk' }, ], [ 'keeps a null const as the enum of a null-only type', { const: null, type: ['null'] }, - { enum: [null], nullable: true }, + { enum: [null] }, ], [ 'matches nothing when a non-null const contradicts a null-only type', { const: 7, type: ['null'] }, - { enum: [7], not: {}, nullable: true }, + { enum: [7], not: {} }, + ], + [ + 'converts a null-only anyOf branch into a null enum', + { anyOf: [{ type: 'string' }, { type: 'null' }] }, + { anyOf: [{ type: 'string' }, { enum: [null] }] }, ], [ 'converts multiple non-null types into anyOf variants', @@ -983,9 +988,25 @@ describe('downgradeSchemaV31ToV30', () => { ['converts a false const', { const: false }, { enum: [false] }], ['converts an empty-string const', { const: '' }, { enum: [''] }], [ - 'converts a null const and marks the schema nullable', + 'converts a null const into a null enum', { const: null }, - { enum: [null], nullable: true }, + { enum: [null] }, + ], + [ + 'keeps the nullable variants of a multi-type null const', + { const: null, type: ['string', 'integer', 'null'] }, + { + anyOf: [ + { nullable: true, type: 'string' }, + { nullable: true, type: 'integer' }, + ], + enum: [null], + }, + ], + [ + 'matches nothing when a null const contradicts a non-null type', + { const: null, type: 'string' }, + { enum: [null], type: 'string' }, ], [ 'replaces an existing enum with the const value', diff --git a/packages/downgrader/src/v3.1-to-v3.0.ts b/packages/downgrader/src/v3.1-to-v3.0.ts index 4736ff0..52f2717 100644 --- a/packages/downgrader/src/v3.1-to-v3.0.ts +++ b/packages/downgrader/src/v3.1-to-v3.0.ts @@ -39,7 +39,6 @@ function applyTypes(types: string[], schema: Record, out: Recor if (!nullable) { return } - out.nullable = true if ('const' in schema) { if (schema.const !== null) { out.not = {} @@ -96,12 +95,8 @@ function convertType(schema: Record, out: Record, out: Record): void { - if (!('const' in schema)) { - return - } - out.enum = [deepClone(schema.const)] - if (schema.const === null) { - out.nullable = true + if ('const' in schema) { + out.enum = [deepClone(schema.const)] } }