Skip to content

Commit 26de8d5

Browse files
committed
refactor: use json-schema-traverse library for schema transformation
- Replace custom recursive traversal with json-schema-traverse library - Library is small (~22KB), has TypeScript types, and is well-tested - Same author as ajv (most popular JSON Schema validator) - Simplifies the addAdditionalPropertiesFalse implementation - All 29 tests still pass
1 parent be1db1c commit 26de8d5

File tree

3 files changed

+26
-59
lines changed

3 files changed

+26
-59
lines changed

pnpm-lock.yaml

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

src/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -465,6 +465,7 @@
465465
"i18next": "^25.0.0",
466466
"ignore": "^7.0.3",
467467
"isbinaryfile": "^5.0.2",
468+
"json-schema-traverse": "^1.0.0",
468469
"jwt-decode": "^4.0.0",
469470
"lodash.debounce": "^4.0.8",
470471
"mammoth": "^1.9.1",

src/utils/json-schema.ts

Lines changed: 16 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { z } from "zod"
2+
import traverse from "json-schema-traverse"
23

34
/**
45
* Type representing a JSON Schema structure
5-
* Defined first so we can reference it in the Zod schema
66
*/
77
export interface JsonSchema {
88
type?: "string" | "number" | "integer" | "boolean" | "null" | "object" | "array"
@@ -85,7 +85,7 @@ export type ValidationResult<T> = { success: true; data: T } | { success: false;
8585
* const transformed = JsonSchemaUtils.addAdditionalPropertiesFalse(result.data)
8686
* }
8787
*
88-
* // Or transform directly (throws on invalid schema)
88+
* // Or transform directly
8989
* const transformed = JsonSchemaUtils.addAdditionalPropertiesFalse(schema)
9090
* ```
9191
*/
@@ -119,7 +119,9 @@ export class JsonSchemaUtils {
119119
* Recursively adds `additionalProperties: false` to all object schemas.
120120
* This is required by some API providers (e.g., OpenAI) for strict function calling.
121121
*
122-
* @param schema - The JSON Schema to transform (can be unvalidated, will be coerced)
122+
* Uses `json-schema-traverse` library for robust schema traversal.
123+
*
124+
* @param schema - The JSON Schema to transform
123125
* @returns A new schema with `additionalProperties: false` on all object schemas
124126
*
125127
* @example
@@ -143,65 +145,20 @@ export class JsonSchemaUtils {
143145
return schema
144146
}
145147

146-
// Create a shallow copy to avoid mutating the original
147-
const result: Record<string, unknown> = { ...schema }
148-
149-
// If this is an object schema, add additionalProperties: false
150-
if (result.type === "object") {
151-
result.additionalProperties = false
152-
}
148+
// Deep clone to avoid mutating the original
149+
const cloned = JSON.parse(JSON.stringify(schema)) as Record<string, unknown>
153150

154-
// Recursively process properties
155-
if (result.properties && typeof result.properties === "object") {
156-
const properties = result.properties as Record<string, unknown>
157-
const newProperties: Record<string, unknown> = {}
158-
for (const key of Object.keys(properties)) {
159-
const value = properties[key]
160-
if (typeof value === "object" && value !== null) {
161-
newProperties[key] = this.addAdditionalPropertiesFalse(value as Record<string, unknown>)
162-
} else {
163-
newProperties[key] = value
151+
// Use json-schema-traverse to visit all schemas and add additionalProperties: false to objects
152+
traverse(cloned, {
153+
allKeys: true,
154+
cb: (subSchema: Record<string, unknown>) => {
155+
if (subSchema.type === "object") {
156+
subSchema.additionalProperties = false
164157
}
165-
}
166-
result.properties = newProperties
167-
}
168-
169-
// Recursively process items (for arrays)
170-
if (result.items && typeof result.items === "object") {
171-
if (Array.isArray(result.items)) {
172-
result.items = result.items.map((item) =>
173-
typeof item === "object" && item !== null
174-
? this.addAdditionalPropertiesFalse(item as Record<string, unknown>)
175-
: item,
176-
)
177-
} else {
178-
result.items = this.addAdditionalPropertiesFalse(result.items as Record<string, unknown>)
179-
}
180-
}
181-
182-
// Recursively process anyOf, oneOf, allOf
183-
for (const keyword of ["anyOf", "oneOf", "allOf"] as const) {
184-
if (Array.isArray(result[keyword])) {
185-
result[keyword] = (result[keyword] as unknown[]).map((subSchema) =>
186-
typeof subSchema === "object" && subSchema !== null
187-
? this.addAdditionalPropertiesFalse(subSchema as Record<string, unknown>)
188-
: subSchema,
189-
)
190-
}
191-
}
192-
193-
// Recursively process additionalProperties if it's a schema (not just true/false)
194-
if (
195-
result.additionalProperties &&
196-
typeof result.additionalProperties === "object" &&
197-
result.additionalProperties !== null
198-
) {
199-
result.additionalProperties = this.addAdditionalPropertiesFalse(
200-
result.additionalProperties as Record<string, unknown>,
201-
)
202-
}
158+
},
159+
})
203160

204-
return result
161+
return cloned
205162
}
206163

207164
/**

0 commit comments

Comments
 (0)