From 480d5bc317397cefcc543723a4ed79eac94f04c7 Mon Sep 17 00:00:00 2001 From: Daniel Eades Date: Sun, 12 Jul 2026 20:13:54 +0100 Subject: [PATCH] Preserve definition-level defaults when types are reached via $ref MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a schema definition carries its own `default` value, that default was dropped when the type was converted via a reference: convert_ref_type recomputed the default from the metadata returned by convert_schema — which is `None` for struct conversions, since TypeEntryStruct::from_metadata consumes the metadata and records the default itself — and unconditionally assigned it, clobbering the already-recorded default. As a result the generated type lacked its `Default` impl (and default-dependent codegen) even though the same schema, inlined, produced one. Only override the recorded default when the metadata returned by the conversion actually specifies one. This preserves the existing precedence: a default declared at the reference site (merged sibling metadata alongside $ref) still comes back as `Some` and takes precedence over the definition-level default. Adds a golden test covering a definition-level default reached via $ref (struct and enum) and a ref-site override. --- typify-impl/src/lib.rs | 18 +- typify/tests/schemas/ref-with-default.json | 48 +++ typify/tests/schemas/ref-with-default.rs | 321 +++++++++++++++++++++ 3 files changed, 384 insertions(+), 3 deletions(-) create mode 100644 typify/tests/schemas/ref-with-default.json create mode 100644 typify/tests/schemas/ref-with-default.rs diff --git a/typify-impl/src/lib.rs b/typify-impl/src/lib.rs index a9cf8c24..fb25fc8f 100644 --- a/typify-impl/src/lib.rs +++ b/typify-impl/src/lib.rs @@ -706,6 +706,12 @@ impl TypeSpace { fn convert_ref_type(&mut self, type_name: Name, schema: Schema, type_id: TypeId) -> Result<()> { let (mut type_entry, metadata) = self.convert_schema(type_name.clone(), &schema)?; + // Note that the conversion may have already recorded a default drawn + // from the definition's own metadata (e.g. structs record it in + // TypeEntryStruct::from_metadata and return no metadata). Only + // override that value if the metadata returned by the conversion + // actually specifies a default--such as one declared at the reference + // site--rather than clobbering it with `None`. let default = metadata .as_ref() .and_then(|m| m.default.as_ref()) @@ -714,15 +720,21 @@ impl TypeSpace { let type_entry = match &mut type_entry.details { // The types that are already named are good to go. TypeEntryDetails::Enum(details) => { - details.default = default; + if default.is_some() { + details.default = default; + } type_entry } TypeEntryDetails::Struct(details) => { - details.default = default; + if default.is_some() { + details.default = default; + } type_entry } TypeEntryDetails::Newtype(details) => { - details.default = default; + if default.is_some() { + details.default = default; + } type_entry } diff --git a/typify/tests/schemas/ref-with-default.json b/typify/tests/schemas/ref-with-default.json new file mode 100644 index 00000000..c64f7863 --- /dev/null +++ b/typify/tests/schemas/ref-with-default.json @@ -0,0 +1,48 @@ +{ + "$comment": "definition-level defaults should survive $ref resolution", + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "DefaultedStruct": { + "type": "object", + "properties": { + "a": { + "type": "string" + } + }, + "additionalProperties": false, + "default": { + "a": "hello" + } + }, + "DefaultedEnum": { + "type": "string", + "enum": [ + "alpha", + "beta", + "gamma" + ], + "default": "beta" + }, + "Root": { + "type": "object", + "properties": { + "via_ref": { + "$ref": "#/definitions/DefaultedStruct" + }, + "enum_via_ref": { + "$ref": "#/definitions/DefaultedEnum" + }, + "override_at_ref": { + "default": { + "a": "override" + }, + "allOf": [ + { + "$ref": "#/definitions/DefaultedStruct" + } + ] + } + } + } + } +} diff --git a/typify/tests/schemas/ref-with-default.rs b/typify/tests/schemas/ref-with-default.rs new file mode 100644 index 00000000..68d76871 --- /dev/null +++ b/typify/tests/schemas/ref-with-default.rs @@ -0,0 +1,321 @@ +#![deny(warnings)] +#[doc = r" Error types."] +pub mod error { + #[doc = r" Error from a `TryFrom` or `FromStr` implementation."] + pub struct ConversionError(::std::borrow::Cow<'static, str>); + impl ::std::error::Error for ConversionError {} + impl ::std::fmt::Display for ConversionError { + fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> Result<(), ::std::fmt::Error> { + ::std::fmt::Display::fmt(&self.0, f) + } + } + impl ::std::fmt::Debug for ConversionError { + fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> Result<(), ::std::fmt::Error> { + ::std::fmt::Debug::fmt(&self.0, f) + } + } + impl From<&'static str> for ConversionError { + fn from(value: &'static str) -> Self { + Self(value.into()) + } + } + impl From for ConversionError { + fn from(value: String) -> Self { + Self(value.into()) + } + } +} +#[doc = "`DefaultedEnum`"] +#[doc = r""] +#[doc = r"
JSON schema"] +#[doc = r""] +#[doc = r" ```json"] +#[doc = "{"] +#[doc = " \"default\": \"beta\","] +#[doc = " \"type\": \"string\","] +#[doc = " \"enum\": ["] +#[doc = " \"alpha\","] +#[doc = " \"beta\","] +#[doc = " \"gamma\""] +#[doc = " ]"] +#[doc = "}"] +#[doc = r" ```"] +#[doc = r"
"] +#[derive( + :: serde :: Deserialize, + :: serde :: Serialize, + Clone, + Copy, + Debug, + Eq, + Hash, + Ord, + PartialEq, + PartialOrd, +)] +pub enum DefaultedEnum { + #[serde(rename = "alpha")] + Alpha, + #[serde(rename = "beta")] + Beta, + #[serde(rename = "gamma")] + Gamma, +} +impl ::std::fmt::Display for DefaultedEnum { + fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { + match *self { + Self::Alpha => f.write_str("alpha"), + Self::Beta => f.write_str("beta"), + Self::Gamma => f.write_str("gamma"), + } + } +} +impl ::std::str::FromStr for DefaultedEnum { + type Err = self::error::ConversionError; + fn from_str(value: &str) -> ::std::result::Result { + match value { + "alpha" => Ok(Self::Alpha), + "beta" => Ok(Self::Beta), + "gamma" => Ok(Self::Gamma), + _ => Err("invalid value".into()), + } + } +} +impl ::std::convert::TryFrom<&str> for DefaultedEnum { + type Error = self::error::ConversionError; + fn try_from(value: &str) -> ::std::result::Result { + value.parse() + } +} +impl ::std::convert::TryFrom<&::std::string::String> for DefaultedEnum { + type Error = self::error::ConversionError; + fn try_from( + value: &::std::string::String, + ) -> ::std::result::Result { + value.parse() + } +} +impl ::std::convert::TryFrom<::std::string::String> for DefaultedEnum { + type Error = self::error::ConversionError; + fn try_from( + value: ::std::string::String, + ) -> ::std::result::Result { + value.parse() + } +} +impl ::std::default::Default for DefaultedEnum { + fn default() -> Self { + DefaultedEnum::Beta + } +} +#[doc = "`DefaultedStruct`"] +#[doc = r""] +#[doc = r"
JSON schema"] +#[doc = r""] +#[doc = r" ```json"] +#[doc = "{"] +#[doc = " \"default\": {"] +#[doc = " \"a\": \"hello\""] +#[doc = " },"] +#[doc = " \"type\": \"object\","] +#[doc = " \"properties\": {"] +#[doc = " \"a\": {"] +#[doc = " \"type\": \"string\""] +#[doc = " }"] +#[doc = " },"] +#[doc = " \"additionalProperties\": false"] +#[doc = "}"] +#[doc = r" ```"] +#[doc = r"
"] +#[derive(:: serde :: Deserialize, :: serde :: Serialize, Clone, Debug)] +#[serde(deny_unknown_fields)] +pub struct DefaultedStruct { + #[serde(default, skip_serializing_if = "::std::option::Option::is_none")] + pub a: ::std::option::Option<::std::string::String>, +} +impl ::std::default::Default for DefaultedStruct { + fn default() -> Self { + DefaultedStruct { + a: ::std::option::Option::Some("hello".to_string()), + } + } +} +impl DefaultedStruct { + pub fn builder() -> builder::DefaultedStruct { + Default::default() + } +} +#[doc = "`Root`"] +#[doc = r""] +#[doc = r"
JSON schema"] +#[doc = r""] +#[doc = r" ```json"] +#[doc = "{"] +#[doc = " \"type\": \"object\","] +#[doc = " \"properties\": {"] +#[doc = " \"enum_via_ref\": {"] +#[doc = " \"$ref\": \"#/definitions/DefaultedEnum\""] +#[doc = " },"] +#[doc = " \"override_at_ref\": {"] +#[doc = " \"default\": {"] +#[doc = " \"a\": \"override\""] +#[doc = " },"] +#[doc = " \"allOf\": ["] +#[doc = " {"] +#[doc = " \"$ref\": \"#/definitions/DefaultedStruct\""] +#[doc = " }"] +#[doc = " ]"] +#[doc = " },"] +#[doc = " \"via_ref\": {"] +#[doc = " \"$ref\": \"#/definitions/DefaultedStruct\""] +#[doc = " }"] +#[doc = " }"] +#[doc = "}"] +#[doc = r" ```"] +#[doc = r"
"] +#[derive(:: serde :: Deserialize, :: serde :: Serialize, Clone, Debug)] +pub struct Root { + #[serde(default, skip_serializing_if = "::std::option::Option::is_none")] + pub enum_via_ref: ::std::option::Option, + #[serde(default = "defaults::root_override_at_ref")] + pub override_at_ref: DefaultedStruct, + #[serde(default, skip_serializing_if = "::std::option::Option::is_none")] + pub via_ref: ::std::option::Option, +} +impl ::std::default::Default for Root { + fn default() -> Self { + Self { + enum_via_ref: Default::default(), + override_at_ref: defaults::root_override_at_ref(), + via_ref: Default::default(), + } + } +} +impl Root { + pub fn builder() -> builder::Root { + Default::default() + } +} +#[doc = r" Types for composing complex structures."] +pub mod builder { + #[derive(Clone, Debug)] + pub struct DefaultedStruct { + a: ::std::result::Result< + ::std::option::Option<::std::string::String>, + ::std::string::String, + >, + } + impl ::std::default::Default for DefaultedStruct { + fn default() -> Self { + Self { + a: Ok(Default::default()), + } + } + } + impl DefaultedStruct { + pub fn a(mut self, value: T) -> Self + where + T: ::std::convert::TryInto<::std::option::Option<::std::string::String>>, + T::Error: ::std::fmt::Display, + { + self.a = value + .try_into() + .map_err(|e| format!("error converting supplied value for a: {e}")); + self + } + } + impl ::std::convert::TryFrom for super::DefaultedStruct { + type Error = super::error::ConversionError; + fn try_from( + value: DefaultedStruct, + ) -> ::std::result::Result { + Ok(Self { a: value.a? }) + } + } + impl ::std::convert::From for DefaultedStruct { + fn from(value: super::DefaultedStruct) -> Self { + Self { a: Ok(value.a) } + } + } + #[derive(Clone, Debug)] + pub struct Root { + enum_via_ref: ::std::result::Result< + ::std::option::Option, + ::std::string::String, + >, + override_at_ref: ::std::result::Result, + via_ref: ::std::result::Result< + ::std::option::Option, + ::std::string::String, + >, + } + impl ::std::default::Default for Root { + fn default() -> Self { + Self { + enum_via_ref: Ok(Default::default()), + override_at_ref: Ok(super::defaults::root_override_at_ref()), + via_ref: Ok(Default::default()), + } + } + } + impl Root { + pub fn enum_via_ref(mut self, value: T) -> Self + where + T: ::std::convert::TryInto<::std::option::Option>, + T::Error: ::std::fmt::Display, + { + self.enum_via_ref = value + .try_into() + .map_err(|e| format!("error converting supplied value for enum_via_ref: {e}")); + self + } + pub fn override_at_ref(mut self, value: T) -> Self + where + T: ::std::convert::TryInto, + T::Error: ::std::fmt::Display, + { + self.override_at_ref = value + .try_into() + .map_err(|e| format!("error converting supplied value for override_at_ref: {e}")); + self + } + pub fn via_ref(mut self, value: T) -> Self + where + T: ::std::convert::TryInto<::std::option::Option>, + T::Error: ::std::fmt::Display, + { + self.via_ref = value + .try_into() + .map_err(|e| format!("error converting supplied value for via_ref: {e}")); + self + } + } + impl ::std::convert::TryFrom for super::Root { + type Error = super::error::ConversionError; + fn try_from(value: Root) -> ::std::result::Result { + Ok(Self { + enum_via_ref: value.enum_via_ref?, + override_at_ref: value.override_at_ref?, + via_ref: value.via_ref?, + }) + } + } + impl ::std::convert::From for Root { + fn from(value: super::Root) -> Self { + Self { + enum_via_ref: Ok(value.enum_via_ref), + override_at_ref: Ok(value.override_at_ref), + via_ref: Ok(value.via_ref), + } + } + } +} +#[doc = r" Generation of default values for serde."] +pub mod defaults { + pub(super) fn root_override_at_ref() -> super::DefaultedStruct { + super::DefaultedStruct { + a: ::std::option::Option::Some("override".to_string()), + } + } +} +fn main() {}