@@ -12,15 +12,17 @@ import * as Record from "./Record.js"
1212import type * as Schema from "./Schema.js"
1313import * as AST from "./SchemaAST.js"
1414
15+ type JsonValue = string | number | boolean | null | Array < JsonValue > | { [ key : string ] : JsonValue }
16+
1517/**
1618 * @category model
1719 * @since 3.10.0
1820 */
1921export interface JsonSchemaAnnotations {
2022 title ?: string
2123 description ?: string
22- default ?: unknown
23- examples ?: Array < unknown >
24+ default ?: JsonValue
25+ examples ?: Array < JsonValue >
2426}
2527
2628/**
@@ -415,9 +417,9 @@ function getRawExamples(annotated: AST.Annotated | undefined): ReadonlyArray<unk
415417 if ( annotated !== undefined ) return Option . getOrUndefined ( AST . getExamplesAnnotation ( annotated ) )
416418}
417419
418- function encodeExamples ( ast : AST . AST , examples : ReadonlyArray < unknown > ) : Array < unknown > | undefined {
420+ function encodeExamples ( ast : AST . AST , examples : ReadonlyArray < unknown > ) : Array < JsonValue > | undefined {
419421 const getOption = ParseResult . getOption ( ast , false )
420- const out = Arr . filterMap ( examples , ( e ) => getOption ( e ) )
422+ const out = Arr . filterMap ( examples , ( e ) => getOption ( e ) . pipe ( Option . filter ( isJsonValue ) ) )
421423 return out . length > 0 ? out : undefined
422424}
423425
@@ -435,6 +437,38 @@ function filterBuiltIn(ast: AST.AST, annotation: string | undefined, key: symbol
435437 return annotation
436438}
437439
440+ function isJsonValue ( value : unknown , visited : Set < unknown > = new Set ( ) ) : value is JsonValue {
441+ if ( value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean" ) {
442+ return true
443+ }
444+ if ( Array . isArray ( value ) || typeof value === "object" ) {
445+ // Check for cyclic references
446+ if ( visited . has ( value ) ) {
447+ return false
448+ }
449+ visited . add ( value )
450+ try {
451+ if ( Array . isArray ( value ) ) {
452+ return value . every ( ( item ) => isJsonValue ( item , visited ) )
453+ }
454+ // Exclude non-plain objects (Date, RegExp, etc.) by checking constructor
455+ const proto = Object . getPrototypeOf ( value )
456+ if ( proto !== null && proto !== Object . prototype ) {
457+ return false
458+ }
459+ // JSON only allows string keys, so exclude objects with Symbol keys
460+ if ( Object . getOwnPropertySymbols ( value ) . length > 0 ) {
461+ return false
462+ }
463+ // Check all values are JSON values
464+ return Object . values ( value ) . every ( ( v ) => isJsonValue ( v , visited ) )
465+ } finally {
466+ visited . delete ( value )
467+ }
468+ }
469+ return false
470+ }
471+
438472function pruneJsonSchemaAnnotations (
439473 ast : AST . AST ,
440474 description : string | undefined ,
@@ -447,7 +481,7 @@ function pruneJsonSchemaAnnotations(
447481 if ( title !== undefined ) out . title = title
448482 if ( Option . isSome ( def ) ) {
449483 const o = encodeDefault ( ast , def . value )
450- if ( Option . isSome ( o ) ) {
484+ if ( Option . isSome ( o ) && isJsonValue ( o . value ) ) {
451485 out . default = o . value
452486 }
453487 }
@@ -949,7 +983,9 @@ function go(
949983 if ( Predicate . isString ( toProperty . title ) ) annotations . title = toProperty . title
950984 if ( Predicate . isString ( toProperty . description ) ) annotations . description = toProperty . description
951985 if ( Array . isArray ( toProperty . examples ) ) annotations . examples = toProperty . examples
952- if ( Object . hasOwn ( toProperty , "default" ) ) annotations . default = toProperty . default
986+ if ( Object . hasOwn ( toProperty , "default" ) && toProperty . default !== undefined ) {
987+ annotations . default = toProperty . default
988+ }
953989 from . properties [ fromKey ] = addAnnotations ( fromProperty , annotations )
954990 }
955991 }
0 commit comments