///|
/// Convert a moon_zod @core.Schema into a standard JSON @core.Schema @core.object.
///
/// Uses JsonSchemaRenderer trait with FullJsonRenderer / SkeletonJsonRenderer / NamedJsonRenderer.
// ── Helper functions (shared across renderers) ──
///|
fn merge_annotations(base : Json, rules : Array[@core.Rule]) -> Json {
guard base is Object(fields) else { return base }
for rule in rules {
if rule.annotation is Object(annotation) {
for key, value in annotation {
match (fields.get(key), value) {
(Some(Number(previous, ..)), Number(next, ..)) =>
if key == "minLength" ||
key == "minItems" ||
key == "minimum" ||
key == "exclusiveMinimum" {
fields.set(
key,
Json::number(if previous > next { previous } else { next }),
)
} else if key == "maxLength" ||
key == "maxItems" ||
key == "maximum" ||
key == "exclusiveMaximum" {
fields.set(
key,
Json::number(if previous < next { previous } else { next }),
)
} else if previous != next {
append_annotation_intersection(fields, key, value)
}
(Some(previous), _) =>
if previous != value {
if key == "type" &&
value == Json::string("integer") &&
previous == Json::string("number") {
fields.set(key, value)
} else {
append_annotation_intersection(fields, key, value)
}
}
(None, _) => fields.set(key, value)
}
}
}
}
base
}
///|
fn append_annotation_intersection(
fields : Map[String, Json],
key : String,
value : Json,
) -> Unit {
let additions = match fields.get("allOf") {
Some(Array(items)) => items
_ => []
}
let annotation : Map[String, Json] = Map([])
annotation.set(key, value)
additions.push(Json::object(annotation))
fields.set("allOf", Json::array(additions))
}
///|
fn json_string_schema() -> Json {
Json::object({ "type": Json::string("string") })
}
///|
fn json_number_schema() -> Json {
Json::object({ "type": Json::string("number") })
}
///|
fn object_allows_additional_properties(mode : @core.ObjectMode) -> Bool {
match mode {
Passthrough => true
Strict | Strip => false
}
}
///|
fn maybe_merge_annotations(
base : Json,
rules : Array[@core.Rule],
include_annotations : Bool,
) -> Json {
if include_annotations {
merge_annotations(base, rules)
} else {
base
}
}
///|
fn add_default(schema_json : Json, default_val : Json) -> Json {
match schema_json {
Object(map) => {
map.set("default", default_val)
schema_json
}
_ => schema_json
}
}
///|
fn[R : JsonSchemaRenderer] render_json_child(
renderer : R,
schema : @core.Schema,
named_names : Array[String],
) -> Json {
if named_names.is_empty() {
render_json_type(renderer, schema)
} else {
render_json_type_ref(renderer, schema, named_names)
}
}
///|
fn[R : JsonSchemaRenderer] render_object_schema(
renderer : R,
spec : Map[String, @core.Schema],
mode : @core.ObjectMode,
schema : @core.Schema,
named_names : Array[String],
include_annotations : Bool,
) -> Json {
let props : Map[String, Json] = Map([])
let required_arr : Array[String] = []
for key, val in spec {
props.set(key, render_json_child(renderer, val, named_names))
if !@core.is_optional_schema(val) {
required_arr.push(key)
}
}
maybe_merge_annotations(
Json::object({
"type": Json::string("object"),
"properties": Json::object(props),
"additionalProperties": Json::boolean(
object_allows_additional_properties(mode),
),
"required": Json::array(required_arr.map(fn(k) { Json::string(k) })),
}),
schema.rules,
include_annotations,
)
}
///|
fn[R : JsonSchemaRenderer] render_array_schema(
renderer : R,
elem : @core.Schema,
schema : @core.Schema,
named_names : Array[String],
include_annotations : Bool,
) -> Json {
maybe_merge_annotations(
Json::object({
"type": Json::string("array"),
"items": render_json_child(renderer, elem, named_names),
}),
schema.rules,
include_annotations,
)
}
///|
fn[R : JsonSchemaRenderer] render_union_schema(
renderer : R,
schemas : Array[@core.Schema],
schema : @core.Schema,
named_names : Array[String],
include_annotations : Bool,
) -> Json {
maybe_merge_annotations(
Json::object({
"anyOf": Json::array(
schemas.map(fn(s) { render_json_child(renderer, s, named_names) }),
),
}),
schema.rules,
include_annotations,
)
}
///|
fn[R : JsonSchemaRenderer] render_intersection_schema(
renderer : R,
schemas : Array[@core.Schema],
schema : @core.Schema,
named_names : Array[String],
include_annotations : Bool,
) -> Json {
if can_merge_object_intersection(schemas) {
return maybe_merge_annotations(
render_merged_object_intersection_schema(renderer, schemas, named_names),
schema.rules,
include_annotations,
)
}
maybe_merge_annotations(
Json::object({
"allOf": Json::array(
schemas.map(fn(s) { render_json_child(renderer, s, named_names) }),
),
}),
schema.rules,
include_annotations,
)
}
///|
fn[R : JsonSchemaRenderer] render_json_tuple_type(
renderer : R,
items : Array[@core.Schema],
) -> Json {
let mut minimum = items.length()
while minimum > 0 && @core.is_optional_schema(items[minimum - 1]) {
minimum = minimum - 1
}
Json::object({
"type": Json::string("array"),
"prefixItems": Json::array(
items.map(fn(item) { render_json_type(renderer, item) }),
),
"minItems": Json::number(minimum.to_double()),
"maxItems": Json::number(items.length().to_double()),
})
}
///|
fn can_merge_object_intersection(schemas : Array[@core.Schema]) -> Bool {
for schema in schemas {
match schema.schema_type {
ObjectType(_, _) => ()
IntersectionType(nested) =>
if !can_merge_object_intersection(nested) {
return false
}
_ => return false
}
}
true
}
///|
fn[R : JsonSchemaRenderer] render_merged_object_intersection_schema(
renderer : R,
schemas : Array[@core.Schema],
named_names : Array[String],
) -> Json {
let props : Map[String, Json] = Map([])
let required_arr : Array[String] = []
collect_object_intersection_parts(
renderer, schemas, named_names, props, required_arr,
)
Json::object({
"type": Json::string("object"),
"properties": Json::object(props),
"additionalProperties": Json::boolean(
object_intersection_allows_additional_properties(schemas),
),
"required": Json::array(required_arr.map(fn(k) { Json::string(k) })),
})
}
///|
fn[R : JsonSchemaRenderer] collect_object_intersection_parts(
renderer : R,
schemas : Array[@core.Schema],
named_names : Array[String],
props : Map[String, Json],
required_arr : Array[String],
) -> Unit {
for schema in schemas {
match schema.schema_type {
ObjectType(spec, _) =>
for key, val in spec {
let rendered = render_json_child(renderer, val, named_names)
match props.get(key) {
Some(existing) =>
props.set(key, combine_property_schema(existing, rendered))
None => props.set(key, rendered)
}
if !@core.is_optional_schema(val) &&
!@core.value_in_array(key, required_arr) {
required_arr.push(key)
}
}
IntersectionType(nested) =>
collect_object_intersection_parts(
renderer, nested, named_names, props, required_arr,
)
_ => ()
}
}
}
///|
fn combine_property_schema(left : Json, right : Json) -> Json {
if left == right {
left
} else {
Json::object({ "allOf": Json::array([left, right]) })
}
}
///|
fn object_intersection_allows_additional_properties(
schemas : Array[@core.Schema],
) -> Bool {
if object_intersection_has_strict(schemas) {
false
} else {
object_intersection_has_passthrough(schemas)
}
}
///|
fn object_intersection_has_strict(schemas : Array[@core.Schema]) -> Bool {
for schema in schemas {
match schema.schema_type {
ObjectType(_, Strict) => return true
IntersectionType(nested) =>
if object_intersection_has_strict(nested) {
return true
}
_ => ()
}
}
false
}
///|
fn object_intersection_has_passthrough(schemas : Array[@core.Schema]) -> Bool {
for schema in schemas {
match schema.schema_type {
ObjectType(_, Passthrough) => return true
IntersectionType(nested) =>
if object_intersection_has_passthrough(nested) {
return true
}
_ => ()
}
}
false
}
// ── NamedJsonRenderer: shared JSON Schema renderer ──
///|
pub struct NamedJsonRenderer {
named_names : Array[String]
include_annotations : Bool
}
///|
pub fn NamedJsonRenderer::NamedJsonRenderer(
named_names~ : Array[String],
include_annotations~ : Bool = true,
) -> NamedJsonRenderer {
{ named_names, include_annotations, }
}
///|
#deprecated
pub extend NamedJsonRenderer with JsonSchemaRenderer::{
render_string,
render_number,
render_boolean,
render_null,
render_object,
render_array,
render_optional,
render_default,
render_enum,
render_union,
render_intersection,
render_transform,
render_literal,
render_any,
render_unknown,
render_tuple,
render_preprocess,
render_lazy,
}
///|
/// The named renderer's methods call render_json_type_ref for sub-elements,
/// which emits $ref for named schemas in the named_names list.
#warnings("-unused_value")
pub impl JsonSchemaRenderer for NamedJsonRenderer with fn render_string(
self : NamedJsonRenderer,
schema : @core.Schema,
) -> Json {
maybe_merge_annotations(
json_string_schema(),
schema.rules,
self.include_annotations,
)
}
///|
#warnings("-unused_value")
pub impl JsonSchemaRenderer for NamedJsonRenderer with fn render_number(
self : NamedJsonRenderer,
schema : @core.Schema,
) -> Json {
maybe_merge_annotations(
json_number_schema(),
schema.rules,
self.include_annotations,
)
}
///|
#warnings("-unused_value")
pub impl JsonSchemaRenderer for NamedJsonRenderer with fn render_boolean(
self : NamedJsonRenderer,
schema : @core.Schema,
) -> Json {
maybe_merge_annotations(
Json::object({ "type": Json::string("boolean") }),
schema.rules,
self.include_annotations,
)
}
///|
#warnings("-unused_value")
pub impl JsonSchemaRenderer for NamedJsonRenderer with fn render_null(
self : NamedJsonRenderer,
schema : @core.Schema,
) -> Json {
maybe_merge_annotations(
Json::object({ "type": Json::string("null") }),
schema.rules,
self.include_annotations,
)
}
///|
pub impl JsonSchemaRenderer for NamedJsonRenderer with fn render_object(
self : NamedJsonRenderer,
spec : Map[String, @core.Schema],
mode : @core.ObjectMode,
schema : @core.Schema,
) -> Json {
render_object_schema(
self,
spec,
mode,
schema,
self.named_names,
self.include_annotations,
)
}
///|
pub impl JsonSchemaRenderer for NamedJsonRenderer with fn render_array(
self : NamedJsonRenderer,
elem : @core.Schema,
schema : @core.Schema,
) -> Json {
render_array_schema(
self,
elem,
schema,
self.named_names,
self.include_annotations,
)
}
///|
#warnings("-unused_value")
pub impl JsonSchemaRenderer for NamedJsonRenderer with fn render_optional(
self : NamedJsonRenderer,
inner : @core.Schema,
schema : @core.Schema,
) -> Json {
render_json_type_ref(
self,
without_inherited_name(schema, inner),
self.named_names,
)
}
///|
#warnings("-unused_value")
pub impl JsonSchemaRenderer for NamedJsonRenderer with fn render_default(
self : NamedJsonRenderer,
inner : @core.Schema,
default_val : Json,
schema : @core.Schema,
) -> Json {
add_default(
render_json_type_ref(
self,
without_inherited_name(schema, inner),
self.named_names,
),
default_val,
)
}
///|
#warnings("-unused_value")
pub impl JsonSchemaRenderer for NamedJsonRenderer with fn render_transform(
self : NamedJsonRenderer,
inner : @core.Schema,
closure : @core.TransformClosure,
schema : @core.Schema,
) -> Json {
let base = render_json_type_ref(
self,
without_inherited_name(schema, inner),
self.named_names,
)
if closure.kind is CustomTransform {
base
} else {
maybe_merge_annotations(base, schema.rules, self.include_annotations)
}
}
///|
#warnings("-unused_value")
pub impl JsonSchemaRenderer for NamedJsonRenderer with fn render_enum(
self : NamedJsonRenderer,
values : Array[String],
schema : @core.Schema,
) -> Json {
maybe_merge_annotations(
Json::object({
"type": Json::string("string"),
"enum": Json::array(values.map(fn(s) { Json::string(s) })),
}),
schema.rules,
self.include_annotations,
)
}
///|
pub impl JsonSchemaRenderer for NamedJsonRenderer with fn render_union(
self : NamedJsonRenderer,
schemas : Array[@core.Schema],
schema : @core.Schema,
) -> Json {
render_union_schema(
self,
schemas,
schema,
self.named_names,
self.include_annotations,
)
}
///|
pub impl JsonSchemaRenderer for NamedJsonRenderer with fn render_intersection(
self : NamedJsonRenderer,
schemas : Array[@core.Schema],
schema : @core.Schema,
) -> Json {
render_intersection_schema(
self,
schemas,
schema,
self.named_names,
self.include_annotations,
)
}
///|
#warnings("-unused_value")
pub impl JsonSchemaRenderer for NamedJsonRenderer with fn render_literal(
self : NamedJsonRenderer,
value : Json,
schema : @core.Schema,
) -> Json {
maybe_merge_annotations(
Json::object({ "const": value }),
schema.rules,
self.include_annotations,
)
}
///|
#warnings("-unused_value")
pub impl JsonSchemaRenderer for NamedJsonRenderer with fn render_any(
self : NamedJsonRenderer,
_schema : @core.Schema,
) -> Json {
Json::empty_object()
}
///|
#warnings("-unused_value")
pub impl JsonSchemaRenderer for NamedJsonRenderer with fn render_unknown(
self : NamedJsonRenderer,
_schema : @core.Schema,
) -> Json {
Json::empty_object()
}
///|
#warnings("-unused_value")
pub impl JsonSchemaRenderer for NamedJsonRenderer with fn render_tuple(
self : NamedJsonRenderer,
items : Array[@core.Schema],
_schema : @core.Schema,
) -> Json {
render_json_tuple_type(self, items)
}
///|
#warnings("-unused_value")
pub impl JsonSchemaRenderer for NamedJsonRenderer with fn render_preprocess(
self : NamedJsonRenderer,
inner : @core.Schema,
schema : @core.Schema,
) -> Json {
render_json_type_ref(
self,
without_inherited_name(schema, inner),
self.named_names,
)
}
// ── Public API ──
///|
/// Convert a moon_zod @core.Schema into a standard JSON @core.Schema @core.object with full
/// constraint annotations (minLength, maximum, pattern, format, etc.).
pub fn to_json_schema(schema : @core.Schema) -> Json {
let _ = @core.collect_named_schemas(schema)
if contains_lazy(schema, Map([])) {
return to_json_schema_named(schema)
}
render_json_type(NamedJsonRenderer(named_names=[]), schema)
}
///|
/// Convert a moon_zod @core.Schema into a lightweight JSON @core.Schema skeleton.
/// Only structural type information is exported.
pub fn to_json_schema_skeleton(schema : @core.Schema) -> Json {
let _ = @core.collect_named_schemas(schema)
if contains_lazy(schema, Map([])) {
return to_json_schema_named_impl(schema, None, false)
}
render_json_type(
NamedJsonRenderer(named_names=[], include_annotations=false),
schema,
)
}
///|
/// Export a JSON @core.Schema with named sub-schemas in a `$defs` section.
/// If root schema has no name, it is assigned the name "Root".
pub fn to_json_schema_named(
schema : @core.Schema,
include_names~ : Array[String]? = None,
) -> Json {
to_json_schema_named_impl(schema, include_names, true)
}
///|
fn to_json_schema_named_impl(
schema : @core.Schema,
include_names : Array[String]?,
include_annotations : Bool,
) -> Json {
let mut schema = schema
if schema.name.is_empty() {
schema = schema.name("Root")
}
let all_named = @core.collect_named_schemas(schema)
let selected_schemas = @core.filter_named_schemas(all_named, include_names)
let named_names : Array[String] = selected_schemas.map(fn(s) { s.name })
let sorted_schemas = @core.topological_sort_schemas(selected_schemas)
let defs_map : Map[String, Json] = Map([])
for ns in sorted_schemas {
let ref_names = named_names
let renderer = NamedJsonRenderer(
named_names=ref_names,
include_annotations~,
)
defs_map.set(ns.name, render_json_type(renderer, ns))
}
let renderer = NamedJsonRenderer(named_names~, include_annotations~)
let root = render_json_type_ref(renderer, schema, named_names)
let result_map : Map[String, Json] = Map([])
match root {
Object(m) =>
for k, v in m {
result_map.set(k, v)
}
_ => ()
}
result_map.set("$defs", Json::object(defs_map))
Json::object(result_map)
}
///|
pub impl JsonSchemaRenderer for NamedJsonRenderer with fn render_lazy(
self,
resolved,
schema,
) {
render_json_type_ref(
self,
without_inherited_name(schema, resolved),
self.named_names,
)
}
///|
fn without_inherited_name(
wrapper : @core.Schema,
inner : @core.Schema,
) -> @core.Schema {
if !wrapper.name.is_empty() && wrapper.name == inner.name {
{ ..inner, name: "", }
} else {
inner
}
}
///|
fn contains_lazy(schema : @core.Schema, visited : Map[Int32, Bool]) -> Bool {
if schema.schema_type is LazyType(_) {
return true
}
if visited.contains(schema.id) {
return false
}
visited.set(schema.id, true)
@core.schema_children(schema).any(fn(child) { contains_lazy(child, visited) })
}