11import { 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 */
77export 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