Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 16 additions & 16 deletions packages/downgrader/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
43 changes: 32 additions & 11 deletions packages/downgrader/src/v3.1-to-v3.0.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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',
Expand Down
9 changes: 2 additions & 7 deletions packages/downgrader/src/v3.1-to-v3.0.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ function applyTypes(types: string[], schema: Record<string, unknown>, out: Recor
if (!nullable) {
return
}
out.nullable = true
if ('const' in schema) {
if (schema.const !== null) {
out.not = {}
Expand Down Expand Up @@ -96,12 +95,8 @@ function convertType(schema: Record<string, unknown>, out: Record<string, unknow
}

function convertConst(schema: Record<string, unknown>, out: Record<string, unknown>): 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)]
}
}

Expand Down
Loading