From 186a63a0dcb7fd94b92f1ccf9412b7c817f306f7 Mon Sep 17 00:00:00 2001 From: Miles Johnson Date: Tue, 20 Jan 2026 20:58:37 -0800 Subject: [PATCH 1/3] Update TS. --- crates/schematic/examples/show_error.rs | 2 ++ .../src/schema/renderers/json_schema.rs | 5 ++- .../src/schema/renderers/typescript.rs | 17 +++++---- ...generator_test__json_schema__partials.snap | 35 ++++++++----------- ...nerator_test__typescript__const_enums.snap | 2 +- .../generator_test__typescript__defaults.snap | 2 +- .../generator_test__typescript__enums.snap | 2 +- ...erator_test__typescript__exclude_refs.snap | 2 +- ...ator_test__typescript__external_types.snap | 2 +- .../generator_test__typescript__no_refs.snap | 2 +- ...ator_test__typescript__object_aliases.snap | 2 +- .../generator_test__typescript__partials.snap | 4 +-- ...ator_test__typescript__props_optional.snap | 2 +- ..._typescript__props_optional_undefined.snap | 2 +- ...nerator_test__typescript__value_enums.snap | 2 +- .../macros_test__generates_json_schema.snap | 35 ++++++++----------- .../macros_test__generates_typescript-2.snap | 4 +-- .../macros_test__generates_typescript.snap | 4 +-- crates/types/src/schema.rs | 21 +++++++++++ 19 files changed, 79 insertions(+), 68 deletions(-) diff --git a/crates/schematic/examples/show_error.rs b/crates/schematic/examples/show_error.rs index c5b621da..cc769d39 100644 --- a/crates/schematic/examples/show_error.rs +++ b/crates/schematic/examples/show_error.rs @@ -1,3 +1,5 @@ +#![allow(unused)] + use schematic::*; #[derive(Config)] diff --git a/crates/schematic/src/schema/renderers/json_schema.rs b/crates/schematic/src/schema/renderers/json_schema.rs index b690cf58..4ec8fb02 100644 --- a/crates/schematic/src/schema/renderers/json_schema.rs +++ b/crates/schematic/src/schema/renderers/json_schema.rs @@ -447,7 +447,10 @@ impl SchemaRenderer for JsonSchemaRenderer { } if field.flatten { - additional_properties = self.render_schema_without_reference(&field.schema)?; + if let Some(schema) = field.schema.get_nonnull_schema() { + additional_properties = self.render_schema_without_reference(schema)?; + } + continue; } diff --git a/crates/schematic/src/schema/renderers/typescript.rs b/crates/schematic/src/schema/renderers/typescript.rs index a70ff7d7..54ed018d 100644 --- a/crates/schematic/src/schema/renderers/typescript.rs +++ b/crates/schematic/src/schema/renderers/typescript.rs @@ -158,8 +158,9 @@ impl TypeScriptRenderer { fn export_object_type(&mut self, name: &str, schema: &Schema, value: String) -> RenderResult { let mut tags = vec![]; - let output = if !value.contains(" & ") - && matches!(self.options.object_format, ObjectFormat::Interface) + let output = if matches!(self.options.object_format, ObjectFormat::Interface) + && value.starts_with('{') + && value.ends_with('}') { format!("export interface {name} {value}") } else { @@ -189,18 +190,16 @@ impl TypeScriptRenderer { // Extract flattened fields first as we'll need to use intersections // to support them correctly in TypeScript for (field_name, field) in &structure.fields { - if field.flatten { + if field.flatten + && let Some(schema) = field.schema.get_nonnull_schema() + { let name = format!( "{name}{}", field_name.from_case(Case::Snake).to_case(Case::Pascal) ); - let value = self.render_schema(&field.schema)?; + let value = self.render_schema(schema)?; - outputs.push(self.export_object_type( - &name, - &field.schema, - format!("{{ [key: string]: {value} }}"), - )?); + outputs.push(self.export_object_type(&name, &field.schema, value)?); extends.push(name); } } diff --git a/crates/schematic/tests/snapshots/generator_test__json_schema__partials.snap b/crates/schematic/tests/snapshots/generator_test__json_schema__partials.snap index 842a712a..9f504c6b 100644 --- a/crates/schematic/tests/snapshots/generator_test__json_schema__partials.snap +++ b/crates/schematic/tests/snapshots/generator_test__json_schema__partials.snap @@ -333,27 +333,20 @@ expression: "fs::read_to_string(file).unwrap()" } }, "additionalProperties": { - "anyOf": [ - { - "type": "object", - "additionalProperties": { - "type": [ - "boolean", - "object", - "array", - "number", - "string", - "integer" - ] - }, - "propertyNames": { - "type": "string" - } - }, - { - "type": "null" - } - ] + "type": "object", + "additionalProperties": { + "type": [ + "boolean", + "object", + "array", + "number", + "string", + "integer" + ] + }, + "propertyNames": { + "type": "string" + } }, "definitions": { "AnotherConfig": { diff --git a/crates/schematic/tests/snapshots/generator_test__typescript__const_enums.snap b/crates/schematic/tests/snapshots/generator_test__typescript__const_enums.snap index 406b14dc..dc53d09a 100644 --- a/crates/schematic/tests/snapshots/generator_test__typescript__const_enums.snap +++ b/crates/schematic/tests/snapshots/generator_test__typescript__const_enums.snap @@ -27,7 +27,7 @@ export interface AnotherConfig { opt: string | null; } -export interface GenConfigFlattened { [key: string]: Record } +export type GenConfigFlattened = Record; /** @deprecated */ export interface GenConfigBase { diff --git a/crates/schematic/tests/snapshots/generator_test__typescript__defaults.snap b/crates/schematic/tests/snapshots/generator_test__typescript__defaults.snap index 12abaf0a..871f68fd 100644 --- a/crates/schematic/tests/snapshots/generator_test__typescript__defaults.snap +++ b/crates/schematic/tests/snapshots/generator_test__typescript__defaults.snap @@ -23,7 +23,7 @@ export interface AnotherConfig { opt: string | null; } -export interface GenConfigFlattened { [key: string]: Record } +export type GenConfigFlattened = Record; /** @deprecated */ export interface GenConfigBase { diff --git a/crates/schematic/tests/snapshots/generator_test__typescript__enums.snap b/crates/schematic/tests/snapshots/generator_test__typescript__enums.snap index 6ccc603a..5cf39bdf 100644 --- a/crates/schematic/tests/snapshots/generator_test__typescript__enums.snap +++ b/crates/schematic/tests/snapshots/generator_test__typescript__enums.snap @@ -27,7 +27,7 @@ export interface AnotherConfig { opt: string | null; } -export interface GenConfigFlattened { [key: string]: Record } +export type GenConfigFlattened = Record; /** @deprecated */ export interface GenConfigBase { diff --git a/crates/schematic/tests/snapshots/generator_test__typescript__exclude_refs.snap b/crates/schematic/tests/snapshots/generator_test__typescript__exclude_refs.snap index a7a90448..5a61e7a2 100644 --- a/crates/schematic/tests/snapshots/generator_test__typescript__exclude_refs.snap +++ b/crates/schematic/tests/snapshots/generator_test__typescript__exclude_refs.snap @@ -20,7 +20,7 @@ export interface AnotherConfig { opt: string | null; } -export interface GenConfigFlattened { [key: string]: Record } +export type GenConfigFlattened = Record; /** @deprecated */ export interface GenConfigBase { diff --git a/crates/schematic/tests/snapshots/generator_test__typescript__external_types.snap b/crates/schematic/tests/snapshots/generator_test__typescript__external_types.snap index fb9dc7e9..838dc4dd 100644 --- a/crates/schematic/tests/snapshots/generator_test__typescript__external_types.snap +++ b/crates/schematic/tests/snapshots/generator_test__typescript__external_types.snap @@ -25,7 +25,7 @@ export interface AnotherConfig { opt: string | null; } -export interface GenConfigFlattened { [key: string]: Record } +export type GenConfigFlattened = Record; /** @deprecated */ export interface GenConfigBase { diff --git a/crates/schematic/tests/snapshots/generator_test__typescript__no_refs.snap b/crates/schematic/tests/snapshots/generator_test__typescript__no_refs.snap index c1b79f86..c5ce3f3f 100644 --- a/crates/schematic/tests/snapshots/generator_test__typescript__no_refs.snap +++ b/crates/schematic/tests/snapshots/generator_test__typescript__no_refs.snap @@ -23,7 +23,7 @@ export interface AnotherConfig { opt: string | null; } -export interface GenConfigFlattened { [key: string]: Record } +export type GenConfigFlattened = Record; /** @deprecated */ export interface GenConfigBase { diff --git a/crates/schematic/tests/snapshots/generator_test__typescript__object_aliases.snap b/crates/schematic/tests/snapshots/generator_test__typescript__object_aliases.snap index 132d2172..be62a54d 100644 --- a/crates/schematic/tests/snapshots/generator_test__typescript__object_aliases.snap +++ b/crates/schematic/tests/snapshots/generator_test__typescript__object_aliases.snap @@ -23,7 +23,7 @@ export type AnotherConfig = { opt: string | null, }; -export type GenConfigFlattened = { [key: string]: Record }; +export type GenConfigFlattened = Record; /** @deprecated */ export type GenConfigBase = { diff --git a/crates/schematic/tests/snapshots/generator_test__typescript__partials.snap b/crates/schematic/tests/snapshots/generator_test__typescript__partials.snap index c8e4db06..b04f3791 100644 --- a/crates/schematic/tests/snapshots/generator_test__typescript__partials.snap +++ b/crates/schematic/tests/snapshots/generator_test__typescript__partials.snap @@ -23,7 +23,7 @@ export interface AnotherConfig { opt: string | null; } -export interface GenConfigFlattened { [key: string]: Record } +export type GenConfigFlattened = Record; /** @deprecated */ export interface GenConfigBase { @@ -83,7 +83,7 @@ export interface PartialAnotherConfig { opt?: string | null; } -export interface PartialGenConfigFlattened { [key: string]: Record | null } +export type PartialGenConfigFlattened = Record; /** @deprecated */ export interface PartialGenConfigBase { diff --git a/crates/schematic/tests/snapshots/generator_test__typescript__props_optional.snap b/crates/schematic/tests/snapshots/generator_test__typescript__props_optional.snap index d6b2ed1d..589ae53c 100644 --- a/crates/schematic/tests/snapshots/generator_test__typescript__props_optional.snap +++ b/crates/schematic/tests/snapshots/generator_test__typescript__props_optional.snap @@ -23,7 +23,7 @@ export interface AnotherConfig { opt?: string | null; } -export interface GenConfigFlattened { [key: string]: Record } +export type GenConfigFlattened = Record; /** @deprecated */ export interface GenConfigBase { diff --git a/crates/schematic/tests/snapshots/generator_test__typescript__props_optional_undefined.snap b/crates/schematic/tests/snapshots/generator_test__typescript__props_optional_undefined.snap index 0e2d5e85..3773e68f 100644 --- a/crates/schematic/tests/snapshots/generator_test__typescript__props_optional_undefined.snap +++ b/crates/schematic/tests/snapshots/generator_test__typescript__props_optional_undefined.snap @@ -23,7 +23,7 @@ export interface AnotherConfig { opt?: string | null | undefined; } -export interface GenConfigFlattened { [key: string]: Record } +export type GenConfigFlattened = Record; /** @deprecated */ export interface GenConfigBase { diff --git a/crates/schematic/tests/snapshots/generator_test__typescript__value_enums.snap b/crates/schematic/tests/snapshots/generator_test__typescript__value_enums.snap index bc8a3dc7..635d569c 100644 --- a/crates/schematic/tests/snapshots/generator_test__typescript__value_enums.snap +++ b/crates/schematic/tests/snapshots/generator_test__typescript__value_enums.snap @@ -27,7 +27,7 @@ export interface AnotherConfig { opt: string | null; } -export interface GenConfigFlattened { [key: string]: Record } +export type GenConfigFlattened = Record; /** @deprecated */ export interface GenConfigBase { diff --git a/crates/schematic/tests/snapshots/macros_test__generates_json_schema.snap b/crates/schematic/tests/snapshots/macros_test__generates_json_schema.snap index 2193dabc..b8bc700d 100644 --- a/crates/schematic/tests/snapshots/macros_test__generates_json_schema.snap +++ b/crates/schematic/tests/snapshots/macros_test__generates_json_schema.snap @@ -835,27 +835,20 @@ expression: "std::fs::read_to_string(file).unwrap()" } }, "additionalProperties": { - "anyOf": [ - { - "type": "object", - "additionalProperties": { - "type": [ - "boolean", - "object", - "array", - "number", - "string", - "integer" - ] - }, - "propertyNames": { - "type": "string" - } - }, - { - "type": "null" - } - ] + "type": "object", + "additionalProperties": { + "type": [ + "boolean", + "object", + "array", + "number", + "string", + "integer" + ] + }, + "propertyNames": { + "type": "string" + } } }, "Serde": { diff --git a/crates/schematic/tests/snapshots/macros_test__generates_typescript-2.snap b/crates/schematic/tests/snapshots/macros_test__generates_typescript-2.snap index 8fbc2720..b56984ac 100644 --- a/crates/schematic/tests/snapshots/macros_test__generates_typescript-2.snap +++ b/crates/schematic/tests/snapshots/macros_test__generates_typescript-2.snap @@ -33,7 +33,7 @@ export const enum Aliased { Baz, } -export type ValueTypesRest = { [key: string]: Record }; +export type ValueTypesRest = Record; export type ValueTypesBase = { boolean: boolean, @@ -197,7 +197,7 @@ export type PartialDefaultValues = { vector?: number[] | null, }; -export type PartialValueTypesRest = { [key: string]: Record | null }; +export type PartialValueTypesRest = Record; export type PartialValueTypesBase = { boolean?: boolean | null, diff --git a/crates/schematic/tests/snapshots/macros_test__generates_typescript.snap b/crates/schematic/tests/snapshots/macros_test__generates_typescript.snap index 524f7482..a22dc1ff 100644 --- a/crates/schematic/tests/snapshots/macros_test__generates_typescript.snap +++ b/crates/schematic/tests/snapshots/macros_test__generates_typescript.snap @@ -16,7 +16,7 @@ export type OtherEnum = 'foo' | 'bar' | 'baz' | string; export type Aliased = 'foo' | 'bar' | 'baz'; -export interface ValueTypesRest { [key: string]: Record } +export type ValueTypesRest = Record; export interface ValueTypesBase { boolean: boolean; @@ -180,7 +180,7 @@ export interface PartialDefaultValues { vector?: number[] | null; } -export interface PartialValueTypesRest { [key: string]: Record | null } +export type PartialValueTypesRest = Record; export interface PartialValueTypesBase { boolean?: boolean | null; diff --git a/crates/types/src/schema.rs b/crates/types/src/schema.rs index 8aeac8bc..86435389 100644 --- a/crates/types/src/schema.rs +++ b/crates/types/src/schema.rs @@ -189,6 +189,27 @@ impl Schema { pub fn set_type(&mut self, value: SchemaType) { self.ty = value; } + + /// Return a non-null schema if available. If a null type, + /// returns `None`. If a union type, returns the first non-null + /// type or `None`. Otherwise, returns the current type. + pub fn get_nonnull_schema(&self) -> Option<&Schema> { + match &self.ty { + SchemaType::Null => None, + SchemaType::Union(inner) => { + for ty in &inner.variants_types { + if ty.is_null() { + continue; + } + + return Some(ty); + } + + None + } + _ => Some(self), + } + } } impl fmt::Display for Schema { From 34c8f35ab2531ec65e9e4ed9543be0efdfd1c8bd Mon Sep 17 00:00:00 2001 From: Miles Johnson Date: Tue, 20 Jan 2026 21:26:52 -0800 Subject: [PATCH 2/3] Fix json schema. --- .../src/schema/renderers/json_schema.rs | 15 +++++-- ...generator_test__json_schema__defaults.snap | 22 ++++------ ...rator_test__json_schema__not_required.snap | 22 ++++------ ...generator_test__json_schema__partials.snap | 44 +++++++------------ ...est__json_schema__with_markdown_descs.snap | 22 ++++------ ...erator_test__json_schema__with_titles.snap | 22 ++++------ .../macros_test__generates_json_schema.snap | 44 +++++++------------ 7 files changed, 76 insertions(+), 115 deletions(-) diff --git a/crates/schematic/src/schema/renderers/json_schema.rs b/crates/schematic/src/schema/renderers/json_schema.rs index 4ec8fb02..60c995bb 100644 --- a/crates/schematic/src/schema/renderers/json_schema.rs +++ b/crates/schematic/src/schema/renderers/json_schema.rs @@ -438,7 +438,7 @@ impl SchemaRenderer for JsonSchemaRenderer { ) -> RenderResult { let mut properties = BTreeMap::new(); let mut required = BTreeSet::from_iter(structure.required.clone().unwrap_or_default()); - let mut additional_properties = JsonSchema::Bool(false); + let mut additional_properties = Some(Box::new(JsonSchema::Bool(false))); let exclude_aliases = self.options.exclude_aliases; for (name, field) in &structure.fields { @@ -448,7 +448,16 @@ impl SchemaRenderer for JsonSchemaRenderer { if field.flatten { if let Some(schema) = field.schema.get_nonnull_schema() { - additional_properties = self.render_schema_without_reference(schema)?; + let flattened = self.render_schema_without_reference(schema)?; + + if matches!(schema.ty, SchemaType::Object(_)) + && let JsonSchema::Object(inner) = flattened.clone() + && let Some(object) = inner.object + { + additional_properties = object.additional_properties; + } else { + additional_properties = Some(Box::new(flattened)); + } } continue; @@ -474,7 +483,7 @@ impl SchemaRenderer for JsonSchemaRenderer { metadata: Some(Box::new(self.create_metadata_from_schema(schema))), instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::Object))), object: Some(Box::new(ObjectValidation { - additional_properties: Some(Box::new(additional_properties)), + additional_properties, required, properties, ..Default::default() diff --git a/crates/schematic/tests/snapshots/generator_test__json_schema__defaults.snap b/crates/schematic/tests/snapshots/generator_test__json_schema__defaults.snap index fb9e0a5d..17c6a258 100644 --- a/crates/schematic/tests/snapshots/generator_test__json_schema__defaults.snap +++ b/crates/schematic/tests/snapshots/generator_test__json_schema__defaults.snap @@ -213,20 +213,14 @@ expression: "fs::read_to_string(file).unwrap()" } }, "additionalProperties": { - "type": "object", - "additionalProperties": { - "type": [ - "boolean", - "object", - "array", - "number", - "string", - "integer" - ] - }, - "propertyNames": { - "type": "string" - } + "type": [ + "boolean", + "object", + "array", + "number", + "string", + "integer" + ] }, "definitions": { "AnotherConfig": { diff --git a/crates/schematic/tests/snapshots/generator_test__json_schema__not_required.snap b/crates/schematic/tests/snapshots/generator_test__json_schema__not_required.snap index f67ad930..0a2de298 100644 --- a/crates/schematic/tests/snapshots/generator_test__json_schema__not_required.snap +++ b/crates/schematic/tests/snapshots/generator_test__json_schema__not_required.snap @@ -184,20 +184,14 @@ expression: "fs::read_to_string(file).unwrap()" } }, "additionalProperties": { - "type": "object", - "additionalProperties": { - "type": [ - "boolean", - "object", - "array", - "number", - "string", - "integer" - ] - }, - "propertyNames": { - "type": "string" - } + "type": [ + "boolean", + "object", + "array", + "number", + "string", + "integer" + ] }, "definitions": { "AnotherConfig": { diff --git a/crates/schematic/tests/snapshots/generator_test__json_schema__partials.snap b/crates/schematic/tests/snapshots/generator_test__json_schema__partials.snap index 9f504c6b..551ee697 100644 --- a/crates/schematic/tests/snapshots/generator_test__json_schema__partials.snap +++ b/crates/schematic/tests/snapshots/generator_test__json_schema__partials.snap @@ -333,20 +333,14 @@ expression: "fs::read_to_string(file).unwrap()" } }, "additionalProperties": { - "type": "object", - "additionalProperties": { - "type": [ - "boolean", - "object", - "array", - "number", - "string", - "integer" - ] - }, - "propertyNames": { - "type": "string" - } + "type": [ + "boolean", + "object", + "array", + "number", + "string", + "integer" + ] }, "definitions": { "AnotherConfig": { @@ -621,20 +615,14 @@ expression: "fs::read_to_string(file).unwrap()" } }, "additionalProperties": { - "type": "object", - "additionalProperties": { - "type": [ - "boolean", - "object", - "array", - "number", - "string", - "integer" - ] - }, - "propertyNames": { - "type": "string" - } + "type": [ + "boolean", + "object", + "array", + "number", + "string", + "integer" + ] } }, "PartialAnotherConfig": { diff --git a/crates/schematic/tests/snapshots/generator_test__json_schema__with_markdown_descs.snap b/crates/schematic/tests/snapshots/generator_test__json_schema__with_markdown_descs.snap index 5e731fe1..7fe258d5 100644 --- a/crates/schematic/tests/snapshots/generator_test__json_schema__with_markdown_descs.snap +++ b/crates/schematic/tests/snapshots/generator_test__json_schema__with_markdown_descs.snap @@ -215,20 +215,14 @@ expression: "fs::read_to_string(file).unwrap()" } }, "additionalProperties": { - "type": "object", - "additionalProperties": { - "type": [ - "boolean", - "object", - "array", - "number", - "string", - "integer" - ] - }, - "propertyNames": { - "type": "string" - } + "type": [ + "boolean", + "object", + "array", + "number", + "string", + "integer" + ] }, "definitions": { "AnotherConfig": { diff --git a/crates/schematic/tests/snapshots/generator_test__json_schema__with_titles.snap b/crates/schematic/tests/snapshots/generator_test__json_schema__with_titles.snap index 03520ae0..5496511b 100644 --- a/crates/schematic/tests/snapshots/generator_test__json_schema__with_titles.snap +++ b/crates/schematic/tests/snapshots/generator_test__json_schema__with_titles.snap @@ -239,20 +239,14 @@ expression: "fs::read_to_string(file).unwrap()" } }, "additionalProperties": { - "type": "object", - "additionalProperties": { - "type": [ - "boolean", - "object", - "array", - "number", - "string", - "integer" - ] - }, - "propertyNames": { - "type": "string" - } + "type": [ + "boolean", + "object", + "array", + "number", + "string", + "integer" + ] }, "definitions": { "AnotherConfig": { diff --git a/crates/schematic/tests/snapshots/macros_test__generates_json_schema.snap b/crates/schematic/tests/snapshots/macros_test__generates_json_schema.snap index b8bc700d..0fa5d853 100644 --- a/crates/schematic/tests/snapshots/macros_test__generates_json_schema.snap +++ b/crates/schematic/tests/snapshots/macros_test__generates_json_schema.snap @@ -835,20 +835,14 @@ expression: "std::fs::read_to_string(file).unwrap()" } }, "additionalProperties": { - "type": "object", - "additionalProperties": { - "type": [ - "boolean", - "object", - "array", - "number", - "string", - "integer" - ] - }, - "propertyNames": { - "type": "string" - } + "type": [ + "boolean", + "object", + "array", + "number", + "string", + "integer" + ] } }, "Serde": { @@ -1026,20 +1020,14 @@ expression: "std::fs::read_to_string(file).unwrap()" } }, "additionalProperties": { - "type": "object", - "additionalProperties": { - "type": [ - "boolean", - "object", - "array", - "number", - "string", - "integer" - ] - }, - "propertyNames": { - "type": "string" - } + "type": [ + "boolean", + "object", + "array", + "number", + "string", + "integer" + ] } } } From 402d1ac03f4687671e92f8c3b8cc037b88c05ce2 Mon Sep 17 00:00:00 2001 From: Miles Johnson Date: Tue, 20 Jan 2026 21:27:53 -0800 Subject: [PATCH 3/3] Update changelog. --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e6c26204..f643a81e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +#### 🐞 Fixes + +- Fixed an incorrect double nested object for flattened fields. + ## 0.19.3 #### 🚀 Updates