Skip to content

Commit e1867cd

Browse files
Copilotbartoszm
andauthored
Add version validation and uplift for unevaluatedProperties in seal-schema (#1)
Add version validation and uplift for unevaluatedProperties Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: bartoszm <1446124+bartoszm@users.noreply.github.com>
1 parent 03dab4a commit e1867cd

File tree

9 files changed

+577
-13
lines changed

9 files changed

+577
-13
lines changed

README.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,12 +140,20 @@ oas-utils seal-schema <input.yaml> -o output.yaml
140140
oas-utils seal-schema <input.yaml> -o output.yaml --use-unevaluated-properties
141141
# Use additionalProperties: false instead
142142
oas-utils seal-schema <input.yaml> -o output.yaml --use-additional-properties
143+
# Automatically upgrade OpenAPI 3.0.x to 3.1.0 or JSON Schema to draft 2020-12
144+
oas-utils seal-schema <input.yaml> -o output.yaml --uplift
143145
```
144146

145147
Options:
146148
- -o, --output: write result to this file (defaults to stdout).
147149
- --use-unevaluated-properties: use `unevaluatedProperties: false` (default, better for JSON Schema 2019-09+).
148150
- --use-additional-properties: use `additionalProperties: false` instead.
151+
- --uplift: automatically upgrade OpenAPI version to 3.1.0 or JSON Schema to draft 2020-12 to support `unevaluatedProperties`.
152+
153+
**Note**: `unevaluatedProperties` is only supported in OpenAPI 3.1+ or JSON Schema 2019-09+. If your document uses an earlier version and you want to use `unevaluatedProperties`, either:
154+
- Use the `--uplift` option to automatically upgrade the version
155+
- Manually upgrade your document to a compatible version
156+
- Use `--use-additional-properties` instead
149157

150158
Example transformation:
151159
- Original `Animal` schema (object-like, referenced in `allOf` by `Cat`)
@@ -193,6 +201,7 @@ decorators:
193201
# Seal object schemas
194202
oas-utils/seal-schema:
195203
useUnevaluatedProperties: true
204+
uplift: false # Set to true to automatically upgrade OpenAPI/JSON Schema version
196205
```
197206

198207
3) Run bundling with Redocly CLI and the decorators will apply the transformations. With `aggressive: true`, unused non-schema components (responses, headers, requestBodies, etc.) are removed as well.
@@ -216,7 +225,7 @@ const result = cleanupDiscriminatorMappings(doc);
216225
console.log(`Removed ${result.mappingsRemoved} invalid mappings from ${result.schemasChecked} schemas`);
217226
218227
// Seal object schemas
219-
sealSchema(doc, { useUnevaluatedProperties: true });
228+
sealSchema(doc, { useUnevaluatedProperties: true, uplift: true });
220229
```
221230

222231
## Notes

package-lock.json

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/cli.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -153,15 +153,20 @@ program
153153
"Use additionalProperties: false instead of unevaluatedProperties: false",
154154
false
155155
)
156+
.option(
157+
"--uplift",
158+
"Automatically upgrade OpenAPI/JSON Schema version to support unevaluatedProperties",
159+
false
160+
)
156161
.action(
157162
async (
158163
input: string | undefined,
159-
opts: { output?: string; useUnevaluatedProperties?: boolean; useAdditionalProperties?: boolean }
164+
opts: { output?: string; useUnevaluatedProperties?: boolean; useAdditionalProperties?: boolean; uplift?: boolean }
160165
) => {
161166
try {
162167
const useUnevaluated = !opts.useAdditionalProperties;
163168
await runSealSchema(
164-
{ output: opts.output, useUnevaluatedProperties: useUnevaluated },
169+
{ output: opts.output, useUnevaluatedProperties: useUnevaluated, uplift: opts.uplift },
165170
format,
166171
() => reader(input)
167172
);

src/lib/cliActions.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -211,14 +211,15 @@ export async function runAllOfToOneOf(
211211
* @param reader - Function to read input
212212
*/
213213
export async function runSealSchema(
214-
opts: { output?: string; useUnevaluatedProperties?: boolean },
214+
opts: { output?: string; useUnevaluatedProperties?: boolean; uplift?: boolean },
215215
format: (doc: any, target?: string) => string,
216216
reader: () => Promise<string>
217217
) {
218218
const doc = parseYamlOrJson(await reader());
219219

220220
const sopts: SealSchemaOptions = {
221221
useUnevaluatedProperties: opts.useUnevaluatedProperties !== false,
222+
uplift: opts.uplift,
222223
};
223224

224225
const result = sealSchema(doc, sopts);

src/lib/oasUtils.ts

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,3 +110,142 @@ export function getAncestors(childName: string, schemas: Record<string, any>): S
110110

111111
return ancestors;
112112
}
113+
114+
/**
115+
* Extracts the OpenAPI version from a document.
116+
*
117+
* @param doc - The OpenAPI document
118+
* @returns The OpenAPI version string (e.g., "3.0.0", "3.1.0") or undefined
119+
*/
120+
export function getOpenApiVersion(doc: any): string | undefined {
121+
if (!doc || typeof doc !== "object") return undefined;
122+
if (typeof doc.openapi === "string") return doc.openapi;
123+
return undefined;
124+
}
125+
126+
/**
127+
* Extracts the JSON Schema version from a document or schema.
128+
*
129+
* @param doc - The JSON Schema document
130+
* @returns The JSON Schema version URI or undefined
131+
*/
132+
export function getJsonSchemaVersion(doc: any): string | undefined {
133+
if (!doc || typeof doc !== "object") return undefined;
134+
if (typeof doc.$schema === "string") return doc.$schema;
135+
return undefined;
136+
}
137+
138+
/**
139+
* Checks if a JSON Schema version supports unevaluatedProperties.
140+
* unevaluatedProperties is supported in draft 2019-09 and later.
141+
*
142+
* @param schemaVersion - The $schema URI (e.g., "http://json-schema.org/draft-07/schema#")
143+
* @returns true if unevaluatedProperties is supported
144+
*/
145+
export function supportsUnevaluatedProperties(schemaVersion: string): boolean {
146+
if (!schemaVersion) return false;
147+
148+
// Check for 2019-09, 2020-12, or later drafts
149+
if (schemaVersion.includes("2019-09") ||
150+
schemaVersion.includes("2020-12") ||
151+
schemaVersion.includes("/next/")) {
152+
return true;
153+
}
154+
155+
// Draft-07 and earlier don't support unevaluatedProperties
156+
// We explicitly return false for these versions
157+
return false;
158+
}
159+
160+
/**
161+
* Checks if an OpenAPI version supports unevaluatedProperties.
162+
* unevaluatedProperties is supported in OpenAPI 3.1 and later.
163+
*
164+
* @param oasVersion - The OpenAPI version (e.g., "3.0.0", "3.1.0")
165+
* @returns true if unevaluatedProperties is supported
166+
*/
167+
export function oasSupportsUnevaluatedProperties(oasVersion: string): boolean {
168+
if (!oasVersion) return false;
169+
170+
// Parse version
171+
const versionMatch = oasVersion.match(/^(\d+)\.(\d+)/);
172+
if (!versionMatch) return false;
173+
174+
const major = parseInt(versionMatch[1], 10);
175+
const minor = parseInt(versionMatch[2], 10);
176+
177+
// OpenAPI 3.1+ supports unevaluatedProperties (uses JSON Schema 2020-12)
178+
if (major === 3 && minor >= 1) return true;
179+
if (major > 3) return true;
180+
181+
return false;
182+
}
183+
184+
/**
185+
* Checks if a document (OpenAPI or JSON Schema) supports unevaluatedProperties.
186+
*
187+
* @param doc - The document to check
188+
* @returns true if unevaluatedProperties is supported
189+
*/
190+
export function documentSupportsUnevaluatedProperties(doc: any): boolean {
191+
if (!doc || typeof doc !== "object") return false;
192+
193+
// Check for OpenAPI version
194+
const oasVersion = getOpenApiVersion(doc);
195+
if (oasVersion) {
196+
return oasSupportsUnevaluatedProperties(oasVersion);
197+
}
198+
199+
// Check for JSON Schema version
200+
const schemaVersion = getJsonSchemaVersion(doc);
201+
if (schemaVersion) {
202+
return supportsUnevaluatedProperties(schemaVersion);
203+
}
204+
205+
// If no version specified, assume it doesn't support unevaluatedProperties
206+
return false;
207+
}
208+
209+
/**
210+
* Upgrades the OpenAPI version to 3.1.0 to support unevaluatedProperties.
211+
*
212+
* @param doc - The OpenAPI document to upgrade
213+
* @returns The upgraded document
214+
*/
215+
export function upgradeToOas31(doc: any): any {
216+
if (!doc || typeof doc !== "object") return doc;
217+
218+
// Only upgrade if it's OpenAPI 3.0.x
219+
const currentVersion = getOpenApiVersion(doc);
220+
if (!currentVersion || !currentVersion.match(/^3\.0\./)) {
221+
return doc;
222+
}
223+
224+
// Upgrade to 3.1.0
225+
doc.openapi = "3.1.0";
226+
227+
return doc;
228+
}
229+
230+
/**
231+
* Upgrades the JSON Schema version to draft 2019-09 to support unevaluatedProperties.
232+
* Only upgrades if the current version is earlier than 2019-09.
233+
*
234+
* @param doc - The JSON Schema document to upgrade
235+
* @returns The upgraded document
236+
*/
237+
export function upgradeJsonSchemaToDraft201909(doc: any): any {
238+
if (!doc || typeof doc !== "object") return doc;
239+
240+
const currentVersion = getJsonSchemaVersion(doc);
241+
242+
// If already supports unevaluatedProperties (2019-09 or later), keep as is
243+
if (currentVersion && supportsUnevaluatedProperties(currentVersion)) {
244+
return doc;
245+
}
246+
247+
// Set $schema to draft 2019-09
248+
doc.$schema = "https://json-schema.org/draft/2019-09/schema";
249+
250+
return doc;
251+
}

src/lib/sealSchema.ts

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,17 @@
1-
import { refToName } from "./oasUtils.js";
1+
import {
2+
refToName,
3+
documentSupportsUnevaluatedProperties,
4+
getOpenApiVersion,
5+
getJsonSchemaVersion,
6+
upgradeToOas31,
7+
upgradeJsonSchemaToDraft201909
8+
} from "./oasUtils.js";
29

310
export interface SealSchemaOptions {
411
/** If true, use unevaluatedProperties: false instead of additionalProperties: false (default: true) */
512
useUnevaluatedProperties?: boolean;
13+
/** If true, automatically upgrade OpenAPI/JSON Schema version to support unevaluatedProperties (default: false) */
14+
uplift?: boolean;
615
}
716

817
/**
@@ -29,6 +38,53 @@ export function sealSchema(doc: any, opts: SealSchemaOptions = {}): any {
2938

3039
// Check if this is a standalone JSON Schema (not an OpenAPI document)
3140
const isStandalone = isStandaloneJsonSchema(doc);
41+
42+
// Check if using unevaluatedProperties and validate version compatibility
43+
if (useUnevaluated) {
44+
const oasVersion = getOpenApiVersion(doc);
45+
const schemaVersion = getJsonSchemaVersion(doc);
46+
47+
// Only validate if there's an explicit version specified
48+
const hasExplicitVersion = oasVersion || schemaVersion;
49+
50+
if (hasExplicitVersion) {
51+
const isCompatible = documentSupportsUnevaluatedProperties(doc);
52+
53+
if (!isCompatible) {
54+
if (opts.uplift) {
55+
// Automatically upgrade the version
56+
if (oasVersion) {
57+
upgradeToOas31(doc);
58+
console.warn(
59+
`[SEAL-SCHEMA] Upgraded OpenAPI version from ${oasVersion} to 3.1.0 to support unevaluatedProperties.`
60+
);
61+
} else if (schemaVersion) {
62+
upgradeJsonSchemaToDraft201909(doc);
63+
console.warn(
64+
`[SEAL-SCHEMA] Upgraded JSON Schema to draft 2019-09 to support unevaluatedProperties.`
65+
);
66+
}
67+
} else {
68+
// Error if uplift is not enabled
69+
const versionInfo = oasVersion
70+
? `OpenAPI ${oasVersion}`
71+
: `JSON Schema ${schemaVersion}`;
72+
73+
throw new Error(
74+
`unevaluatedProperties is only supported in OpenAPI 3.1+ or JSON Schema 2019-09+. ` +
75+
`Current document uses ${versionInfo}. ` +
76+
`Use --uplift option to automatically upgrade the version, or use --use-additional-properties instead.`
77+
);
78+
}
79+
}
80+
} else if (opts.uplift && isStandalone) {
81+
// If no version and it's a standalone schema, set it when uplift is enabled
82+
upgradeJsonSchemaToDraft201909(doc);
83+
console.warn(
84+
`[SEAL-SCHEMA] Set JSON Schema version to draft 2019-09 to support unevaluatedProperties.`
85+
);
86+
}
87+
}
3288

3389
let schemas: Record<string, any> | undefined;
3490
let wrappedName: string = "";

src/redocly/seal-schema-decorator.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export default function SealSchemaDecorator(opts: any) {
66
leave(target: any) {
77
sealSchema(target, {
88
useUnevaluatedProperties: opts?.useUnevaluatedProperties !== false,
9+
uplift: opts?.uplift === true,
910
});
1011
},
1112
},

0 commit comments

Comments
 (0)