Repository: oxidecomputer/typify
Pull request #292 (2023-05-12 01:47:57)

support for reflexive schemas
This adds handling for structures like this:

```json
{
  "$ref": "#"
}
```

This refers to the root schema itself.


Patch:
diff --git a/typify-impl/src/convert.rs b/typify-impl/src/convert.rs
index fffff96..c51fd48 100644
--- a/typify-impl/src/convert.rs
+++ b/typify-impl/src/convert.rs
@@ -1,6 +1,6 @@
 // Copyright 2023 Oxide Computer Company
 
-use std::collections::HashSet;
+use std::collections::BTreeSet;
 
 use crate::type_entry::{
     EnumTagType, TypeEntry, TypeEntryDetails, TypeEntryEnum, TypeEntryNewtype, TypeEntryStruct,
@@ -364,7 +364,7 @@ impl TypeSpace {
                 reference: Some(reference),
                 extensions: _,
             } => {
-                let ref_schema = self.definitions.get(ref_key(reference)).unwrap();
+                let ref_schema = self.definitions.get(&ref_key(reference)).unwrap();
                 assert!(matches!(ref_schema, Schema::Object(SchemaObject {
                         instance_type: it, ..
                     }) if instance_type == it));
@@ -500,66 +500,126 @@ impl TypeSpace {
                 Ok((type_entry, metadata))
             }
 
-            // If we have a simple type--complicated only by accepting multiple
-            // types (none duplicated)--we can create an enum; note that the
-            // case of a 2-type list with one of them Null is already handled
-            // above (and rendered into an Option).
+            // Turn schemas with multiple types into an untagged enum labeled
+            // according to the given type. We associate any validation with
+            // the appropriate type. Note that the case of a 2-type list with
+            // one of them Null is already handled more specifically above (and
+            // rendered into an Option type).
             SchemaObject {
                 metadata,
                 instance_type: Some(SingleOrVec::Vec(instance_types)),
-                format: None,
+                format,
                 enum_values: None,
                 const_value: None,
                 subschemas: None,
-                number: None,
-                string: None,
-                array: None,
-                object: None,
+                number,
+                string,
+                array,
+                object,
                 reference: None,
                 extensions: _,
-            } if instance_types.len() == instance_types.iter().collect::<HashSet<_>>().len() => {
-                let variants = instance_types
-                    .iter()
+            } => {
+                // Eliminate duplicates (they hold no significance).
+                let unique_types = instance_types.iter().collect::<BTreeSet<_>>();
+
+                // Massage the data into labeled subschemas with the following
+                // format:
+                //
+                // {
+                //     "title": <instance type name>,
+                //     "allOf": [
+                //         {
+                //             "type": <instance type>,
+                //             <validation relevant to the type>
+                //         }
+                //     ]
+                // }
+                //
+                // We can then take these and construct an untagged enum. The
+                // outer "allOf" schema lets name the variant.
+                //
+                // Note that we *could* simply copy the full schema, trusting
+                // recursive calls to pull out the appropriate components...
+                // but why do tomorrow what we could easily to today?
+                let subschemas = unique_types
+                    .into_iter()
                     .map(|it| {
-                        let (name, maybe_ty) = match it {
-                            InstanceType::Null => ("Null", None),
-                            InstanceType::Boolean => ("Boolean", Some(TypeEntry::new_boolean())),
-                            InstanceType::Object => {
-                                let (ty, _) = self.make_map(None, &None, &None)?;
-                                ("Object", Some(ty))
-                            }
-                            InstanceType::Array => {
-                                let (ty, _) = self.convert_array_of_any(&None)?;
-                                ("Array", Some(ty))
-                            }
-                            InstanceType::Number => ("Number", Some(TypeEntry::new_float("f64"))),
-                            InstanceType::String => {
-                                ("String", Some(TypeEntry::from(TypeEntryDetails::String)))
-                            }
-                            InstanceType::Integer => {
-                                ("Integer", Some(TypeEntry::new_integer("i64")))
-                            }
-                        };
-                        let details = match maybe_ty {
-                            Some(ty) => VariantDetails::Item(self.assign_type(ty)),
-                            None => VariantDetails::Simple,
+                        let instance_type = Some(SingleOrVec::Single(Box::new(*it)));
+                        let (label, inner_schema) = match it {
+                            InstanceType::Null => (
+                                "null",
+                                SchemaObject {
+                                    instance_type,
+                                    ..Default::default()
+                                },
+                            ),
+                            InstanceType::Boolean => (
+                                "boolean",
+                                SchemaObject {
+                                    instance_type,
+                                    ..Default::default()
+                                },
+                            ),
+                            InstanceType::Object => (
+                                "object",
+                                SchemaObject {
+                                    instance_type,
+                                    object: object.clone(),
+                                    ..Default::default()
+                                },
+                            ),
+                            InstanceType::Array => (
+                                "array",
+                                SchemaObject {
+                                    instance_type,
+                                    array: array.clone(),
+                                    ..Default::default()
+                                },
+                            ),
+                            InstanceType::Number => (
+                                "number",
+                                SchemaObject {
+                                    instance_type,
+                                    format: format.clone(),
+                                    number: number.clone(),
+                                    ..Default::default()
+                                },
+                            ),
+                            InstanceType::String => (
+                                "string",
+                                SchemaObject {
+                                    instance_type,
+                                    format: format.clone(),
+                                    string: string.clone(),
+                                    ..Default::default()
+                                },
+                            ),
+                            InstanceType::Integer => (
+                                "integer",
+                                SchemaObject {
+                                    instance_type,
+                                    format: format.clone(),
+                                    number: number.clone(),
+                                    ..Default::default()
+                                },
+                            ),
                         };
-                        Ok(Variant {
-                            name: name.to_string(),
-                            rename: None,
-                            description: None,
-                            details,
+                        // Make the wrapping schema.
+                        Schema::Object(SchemaObject {
+                            metadata: Some(Box::new(Metadata {
+                                title: Some(label.to_string()),
+                                ..Default::default()
+                            })),
+                            subschemas: Some(Box::new(SubschemaValidation {
+                                all_of: Some(vec![inner_schema.into()]),
+                                ..Default::default()
+                            })),
+                            ..Default::default()
                         })
                     })
-                    .collect::<Result<_>>()?;
-                let type_entry = TypeEntryEnum::from_metadata(
-                    self,
-                    type_name,
-                    metadata,
-                    EnumTagType::Untagged,
-                    variants,
-                    false,
-                );
+                    .collect::<Vec<_>>();
+
+                let type_entry = self.untagged_enum(type_name, metadata, &subschemas)?;
                 Ok((type_entry, metadata))
             }
 
@@ -965,7 +1025,7 @@ impl TypeSpace {
         let key = ref_key(ref_name);
         let type_id = self
             .ref_to_id
-            .get(key)
+            .get(&key)
             .unwrap_or_else(|| panic!("$ref {} is missing", ref_name));
         Ok((
             TypeEntryDetails::Reference(type_id.clone()).into(),
@@ -1207,7 +1267,7 @@ impl TypeSpace {
                             panic!("unhandled type for `not` construction: {}", v)
                         }
                     })
-                    .collect::<HashSet<_>>();
+                    .collect::<BTreeSet<_>>();
 
                 match (instance_types.len(), instance_types.iter().next()) {
                     (1, Some(instance_type)) => {
@@ -1438,7 +1498,7 @@ impl TypeSpace {
                 serde_json::Value::Array(_) => InstanceType::Array,
                 serde_json::Value::Object(_) => InstanceType::Object,
             })
-            .collect::<HashSet<_>>();
+            .collect::<BTreeSet<_>>();
 
         let has_null = instance_types.remove(&InstanceType::Null);
 
diff --git a/typify-impl/src/enums.rs b/typify-impl/src/enums.rs
index a554e95..587ce76 100644
--- a/typify-impl/src/enums.rs
+++ b/typify-impl/src/enums.rs
@@ -805,7 +805,7 @@ mod tests {
         output::OutputSpace,
         test_util::{validate_output, validate_output_for_untagged_enm},
         type_entry::{EnumTagType, TypeEntryEnum, Variant, VariantDetails},
-        Name, TypeEntryDetails, TypeId, TypeSpace, TypeSpaceSettings,
+        Name, RefKey, TypeEntryDetails, TypeId, TypeSpace, TypeSpaceSettings,
     };
 
     #[allow(dead_code)]
@@ -1246,7 +1246,10 @@ mod tests {
         let mut type_space = TypeSpace::default();
         type_space.add_ref_types(schema.definitions).unwrap();
 
-        let type_id = type_space.ref_to_id.get("workflow-step").unwrap();
+        let type_id = type_space
+            .ref_to_id
+            .get(&RefKey::Def("workflow-step".to_string()))
+            .unwrap();
         let type_entry = type_space.id_to_entry.get(type_id).unwrap();
 
         match &type_entry.details {
@@ -1342,7 +1345,10 @@ mod tests {
         let mut type_space = TypeSpace::default();
         type_space.add_ref_types(schema.definitions).unwrap();
 
-        let type_id = type_space.ref_to_id.get("workflow-step").unwrap();
+        let type_id = type_space
+            .ref_to_id
+            .get(&RefKey::Def("workflow-step".to_string()))
+            .unwrap();
         let type_entry = type_space.id_to_entry.get(type_id).unwrap();
 
         match &type_entry.details {
diff --git a/typify-impl/src/lib.rs b/typify-impl/src/lib.rs
index 5c3f2be..73448b9 100644
--- a/typify-impl/src/lib.rs
+++ b/typify-impl/src/lib.rs
@@ -133,6 +133,12 @@ impl Name {
     }
 }
 
+#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)]
+pub(crate) enum RefKey {
+    Root,
+    Def(String),
+}
+
 /// A collection of types.
 #[derive(Debug)]
 pub struct TypeSpace {
@@ -142,13 +148,13 @@ pub struct TypeSpace {
     // e.g. to do `all_mutually_exclusive`. In the future, we could obviate the
     // need this by keeping a single Map of referenced types whose value was an
     // enum of a "raw" or a "converted" schema.
-    definitions: BTreeMap<String, Schema>,
+    definitions: BTreeMap<RefKey, Schema>,
 
     id_to_entry: BTreeMap<TypeId, TypeEntry>,
     type_to_id: BTreeMap<TypeEntryDetails, TypeId>,
 
     name_to_id: BTreeMap<String, TypeId>,
-    ref_to_id: BTreeMap<String, TypeId>,
+    ref_to_id: BTreeMap<RefKey, TypeId>,
 
     uses_chrono: bool,
     uses_uuid: bool,
@@ -370,12 +376,20 @@ impl TypeSpace {
     where
         I: IntoIterator<Item = (S, Schema)>,
         S: AsRef<str>,
+    {
+        self.add_ref_types_impl(
+            type_defs
+                .into_iter()
+                .map(|(key, schema)| (RefKey::Def(key.as_ref().to_string()), schema)),
+        )
+    }
+
+    fn add_ref_types_impl<I>(&mut self, type_defs: I) -> Result<()>
+    where
+        I: IntoIterator<Item = (RefKey, Schema)>,
     {
         // Gather up all types to make things a little more convenient.
-        let definitions = type_defs
-            .into_iter()
-            .map(|(name, schema)| (name.as_ref().to_string(), schema))
-            .collect::<Vec<(String, Schema)>>();
+        let definitions = type_defs.into_iter().collect::<Vec<_>>();
 
         // Assign IDs to reference types before actually converting them. We'll
         // need these in the case of forward (or circular) references.
@@ -385,7 +399,7 @@ impl TypeSpace {
 
         for (index, (ref_name, schema)) in definitions.iter().enumerate() {
             self.ref_to_id
-                .insert(ref_name.to_string(), TypeId(base_id + index as u64));
+                .insert(ref_name.clone(), TypeId(base_id + index as u64));
             self.definitions.insert(ref_name.clone(), schema.clone());
         }
 
@@ -393,19 +407,34 @@ impl TypeSpace {
         // previous step because each type may create additional types. This
         // effectively is doing the work of `add_type_with_name` but for a
         // batch of types.
-        for (index, (type_name, schema)) in definitions.into_iter().enumerate() {
+        for (index, (ref_name, schema)) in definitions.into_iter().enumerate() {
             info!(
-                "converting type: {} with schema {}",
-                type_name,
+                "converting type: {:?} with schema {}",
+                ref_name,
                 serde_json::to_string(&schema).unwrap()
             );
 
             // Check for manually replaced types. Proceed with type conversion
             // if there is none; use the specified type if there is.
             let type_id = TypeId(base_id + index as u64);
-            let check_name = sanitize(&type_name, Case::Pascal);
-            match self.settings.replace.get(&check_name) {
-                None => self.convert_ref_type(&type_name, schema, type_id)?,
+
+            let maybe_replace = match &ref_name {
+                RefKey::Root => None,
+                RefKey::Def(def_name) => {
+                    let check_name = sanitize(def_name, Case::Pascal);
+                    self.settings.replace.get(&check_name)
+                }
+            };
+
+            match maybe_replace {
+                None => {
+                    let type_name = if let RefKey::Def(name) = ref_name {
+                        Name::Required(name.clone())
+                    } else {
+                        Name::Unknown
+                    };
+                    self.convert_ref_type(type_name, schema, type_id)?
+                }
 
                 Some(replace_type) => {
                     let type_entry = TypeEntry::new_native(
@@ -456,9 +485,8 @@ impl TypeSpace {
         Ok(())
     }
 
-    fn convert_ref_type(&mut self, type_name: &str, schema: Schema, type_id: TypeId) -> Result<()> {
-        let (mut type_entry, metadata) =
-            self.convert_schema(Name::Required(type_name.to_string()), &schema)?;
+    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)?;
         let default = metadata
             .as_ref()
             .and_then(|m| m.default.as_ref())
@@ -483,24 +511,15 @@ impl TypeSpace {
             // simple alias to another type in this list of definitions
             // (which may nor may not have already been converted). We
             // simply create a newtype with that type ID.
-            TypeEntryDetails::Reference(type_id) => TypeEntryNewtype::from_metadata(
-                self,
-                Name::Required(type_name.to_string()),
-                metadata,
-                type_id.clone(),
-            ),
+            TypeEntryDetails::Reference(type_id) => {
+                TypeEntryNewtype::from_metadata(self, type_name, metadata, type_id.clone())
+            }
 
             // For types that don't have names, this is effectively a type
             // alias which we treat as a newtype.
             _ => {
                 let subtype_id = self.assign_type(type_entry);
-
-                TypeEntryNewtype::from_metadata(
-                    self,
-                    Name::Required(type_name.to_string()),
-                    metadata,
-                    subtype_id,
-                )
+                TypeEntryNewtype::from_metadata(self, type_name, metadata, subtype_id)
             }
         };
         let entry_name = type_entry.name().unwrap().clone();
@@ -649,12 +668,32 @@ impl TypeSpace {
     /// referenced types and the top-level type (if there is one and it has a
     /// title).
     pub fn add_root_schema(&mut self, schema: RootSchema) -> Result<Option<TypeId>> {
-        self.add_ref_types(schema.definitions)?;
+        let RootSchema {
+            meta_schema: _,
+            schema,
+            definitions,
+        } = schema;
+
+        let mut defs = definitions
+            .into_iter()
+            .map(|(key, schema)| (RefKey::Def(key), schema))
+            .collect::<Vec<_>>();
+
+        // Does the root type have a name (otherwise... ignore it)
+        let root_type = schema
+            .metadata
+            .as_ref()
+            .and_then(|m| m.title.as_ref())
+            .is_some();
+
+        if root_type {
+            defs.push((RefKey::Root, schema.into()));
+        }
+
+        self.add_ref_types_impl(defs)?;
 
-        // Only convert the top-level type if it has a name
-        if let Some(type_name) = (|| schema.schema.metadata.as_ref()?.title.as_ref().cloned())() {
-            self.add_type_with_name(&Schema::Object(schema.schema), Some(type_name))
-                .map(Some)
+        if root_type {
+            Ok(self.ref_to_id.get(&RefKey::Root).cloned())
         } else {
             Ok(None)
         }
diff --git a/typify-impl/src/structs.rs b/typify-impl/src/structs.rs
index 51cafd8..f537a11 100644
--- a/typify-impl/src/structs.rs
+++ b/typify-impl/src/structs.rs
@@ -420,10 +420,7 @@ impl TypeSpace {
         }
 
         let (named_schema, _) = named.first()?;
-        let (a_name, _, validation) =
-            get_object_ref(Name::Unknown, named_schema, &self.definitions)?;
-
-        assert!(matches!(a_name, Name::Required(_)));
+        let validation = get_object_ref(named_schema, &self.definitions)?;
 
         // We need all unnamed schemas to be a subset of the named schema.
         if !unnamed
diff --git a/typify-impl/src/util.rs b/typify-impl/src/util.rs
index 94fcf74..3982fe4 100644
--- a/typify-impl/src/util.rs
+++ b/typify-impl/src/util.rs
@@ -8,7 +8,7 @@ use schemars::schema::{
 };
 use unicode_ident::{is_xid_continue, is_xid_start};
 
-use crate::{Error, Name, Result, TypeSpace};
+use crate::{Error, Name, RefKey, Result, TypeSpace};
 
 pub(crate) fn metadata_description(metadata: &Option<Box<Metadata>>) -> Option<String> {
     metadata
@@ -35,7 +35,7 @@ pub(crate) fn metadata_title_and_description(metadata: &Option<Box<Metadata>>) -
 
 pub(crate) fn all_mutually_exclusive(
     subschemas: &[Schema],
-    definitions: &schemars::Map<String, Schema>,
+    definitions: &schemars::Map<RefKey, Schema>,
 ) -> bool {
     let len = subschemas.len();
     // Consider all pairs
@@ -483,14 +483,17 @@ pub(crate) fn constant_string_value(schema: &Schema) -> Option<&str> {
     }
 }
 
-pub(crate) fn ref_key(ref_name: &str) -> &str {
-    match ref_name.rfind('/') {
-        Some(idx) => &ref_name[idx + 1..],
-        None => ref_name,
+pub(crate) fn ref_key(ref_name: &str) -> RefKey {
+    if ref_name == "#" {
+        RefKey::Root
+    } else if let Some(idx) = ref_name.rfind('/') {
+        RefKey::Def(ref_name[idx + 1..].to_string())
+    } else {
+        panic!("expected a '/' in $ref: {}", ref_name)
     }
 }
 
-fn resolve<'a>(schema: &'a Schema, definitions: &'a schemars::Map<String, Schema>) -> &'a Schema {
+fn resolve<'a>(schema: &'a Schema, definitions: &'a schemars::Map<RefKey, Schema>) -> &'a Schema {
     match schema {
         Schema::Bool(_) => schema,
         Schema::Object(SchemaObject {
@@ -506,7 +509,7 @@ fn resolve<'a>(schema: &'a Schema, definitions: &'a schemars::Map<String, Schema
             object: None,
             reference: Some(ref_name),
             extensions: _,
-        }) => definitions.get(ref_key(ref_name)).unwrap(),
+        }) => definitions.get(&ref_key(ref_name)).unwrap(),
         Schema::Object(SchemaObject {
             reference: None, ..
         }) => schema,
@@ -637,17 +640,17 @@ pub(crate) fn get_object(schema: &Schema) -> Option<(&Option<Box<Metadata>>, &Ob
         _ => None,
     }
 }
+
 /// Return the object data or None if it's not an object (or doesn't conform to
 /// the objects we know how to handle). Follow references if we find them.
 pub(crate) fn get_object_ref<'a>(
-    type_name: Name,
     schema: &'a Schema,
-    definitions: &'a BTreeMap<String, Schema>,
-) -> Option<(Name, &'a Option<Box<Metadata>>, &'a ObjectValidation)> {
+    definitions: &'a BTreeMap<RefKey, Schema>,
+) -> Option<&'a ObjectValidation> {
     match schema {
         // Objects
         Schema::Object(SchemaObject {
-            metadata,
+            metadata: _,
             instance_type: Some(SingleOrVec::Single(single)),
             format: None,
             enum_values: None,
@@ -666,7 +669,7 @@ pub(crate) fn get_object_ref<'a>(
             && validation.pattern_properties.is_empty()
             && validation.property_names.is_none() =>
         {
-            Some((type_name, metadata, validation.as_ref()))
+            Some(validation.as_ref())
         }
 
         // References
@@ -685,16 +688,12 @@ pub(crate) fn get_object_ref<'a>(
             extensions: _,
         }) => {
             let ref_key = ref_key(ref_name);
-            get_object_ref(
-                Name::Required(ref_key.to_string()),
-                definitions.get(ref_key).unwrap(),
-                definitions,
-            )
+            get_object_ref(definitions.get(&ref_key).unwrap(), definitions)
         }
 
         // Trivial (n == 1) subschemas
         Schema::Object(SchemaObject {
-            metadata,
+            metadata: _,
             instance_type: _,
             format: None,
             enum_values: None,
@@ -706,14 +705,8 @@ pub(crate) fn get_object_ref<'a>(
             object: None,
             reference: None,
             extensions: _,
-        }) => singleton_subschema(subschemas).and_then(|sub_schema| {
-            get_object_ref(type_name, sub_schema, definitions).map(
-                |(name, m, validation)| match m {
-                    Some(_) => (name, metadata, validation),
-                    None => (name, &None, validation),
-                },
-            )
-        }),
+        }) => singleton_subschema(subschemas)
+            .and_then(|sub_schema| get_object_ref(sub_schema, definitions)),
 
         // None if the schema doesn't match the shape we expect.
         _ => None,


Test patch:
diff --git a/typify-impl/src/test_util.rs b/typify-impl/src/test_util.rs
index 7d0f831..3642168 100644
--- a/typify-impl/src/test_util.rs
+++ b/typify-impl/src/test_util.rs
@@ -12,7 +12,7 @@ use syn::{
     FieldsNamed, FieldsUnnamed, File, Type, TypePath, TypeTuple, Variant,
 };
 
-use crate::{output::OutputSpace, Name, TypeId, TypeSpace};
+use crate::{output::OutputSpace, Name, RefKey, TypeId, TypeSpace};
 
 pub(crate) fn get_type<T: JsonSchema>() -> (TypeSpace, TypeId) {
     let schema = schema_for!(T);
@@ -33,17 +33,18 @@ pub(crate) fn get_type<T: JsonSchema>() -> (TypeSpace, TypeId) {
     // In some situations, `schema_for!(T)` may actually give us two copies
     // of the type: one in the definitions and one in the schema. This will
     // occur in particular for cyclic types i.e. those for which the type
-    // itself is a reference.
+    // itself is a reference. This is arguably a bug in schemars.
     //
     // If we have converted the type already, use that, otherwise convert
     // schema object
-    let type_id = if let Some(already_type_id) = type_space.ref_to_id.get(&name) {
-        already_type_id.clone()
-    } else {
-        type_space
-            .add_type_with_name(&schema.schema.into(), Some(name.to_string()))
-            .unwrap()
-    };
+    let type_id =
+        if let Some(already_type_id) = type_space.ref_to_id.get(&RefKey::Def(name.clone())) {
+            already_type_id.clone()
+        } else {
+            type_space
+                .add_type_with_name(&schema.schema.into(), Some(name.clone()))
+                .unwrap()
+        };
 
     (type_space, type_id)
 }
diff --git a/typify/tests/schemas/multiple-instance-types.json b/typify/tests/schemas/multiple-instance-types.json
index 144374e..c5569b8 100644
--- a/typify/tests/schemas/multiple-instance-types.json
+++ b/typify/tests/schemas/multiple-instance-types.json
@@ -27,6 +27,17 @@
         "string",
         "integer"
       ]
+    },
+    "yes-no-maybe": {
+      "type": [
+        "boolean",
+        "object"
+      ],
+      "properties": {
+        "value": {
+          "type": "string"
+        }
+      }
     }
   }
 }
\ No newline at end of file
diff --git a/typify/tests/schemas/multiple-instance-types.rs b/typify/tests/schemas/multiple-instance-types.rs
index 957dcd9..ce52eb8 100644
--- a/typify/tests/schemas/multiple-instance-types.rs
+++ b/typify/tests/schemas/multiple-instance-types.rs
@@ -112,4 +112,23 @@ impl From<serde_json::Value> for SeriouslyAnything {
         Self(value)
     }
 }
+#[derive(Clone, Debug, Deserialize, Serialize)]
+#[serde(untagged)]
+pub enum YesNoMaybe {
+    Boolean(bool),
+    Object {
+        #[serde(default, skip_serializing_if = "Option::is_none")]
+        value: Option<String>,
+    },
+}
+impl From<&YesNoMaybe> for YesNoMaybe {
+    fn from(value: &YesNoMaybe) -> Self {
+        value.clone()
+    }
+}
+impl From<bool> for YesNoMaybe {
+    fn from(value: bool) -> Self {
+        Self::Boolean(value)
+    }
+}
 fn main() {}
diff --git a/typify/tests/schemas/reflexive.json b/typify/tests/schemas/reflexive.json
new file mode 100644
index 0000000..7eb4e6e
--- /dev/null
+++ b/typify/tests/schemas/reflexive.json
@@ -0,0 +1,17 @@
+{
+  "$comment": "validate references to the whole",
+  "$schema": "http://json-schema.org/draft-07/schema#",
+  "title": "node",
+  "type": "object",
+  "properties": {
+    "value": {
+      "type": "integer"
+    },
+    "children": {
+      "type": "array",
+      "items": {
+        "$ref": "#"
+      }
+    }
+  }
+}
\ No newline at end of file
diff --git a/typify/tests/schemas/reflexive.rs b/typify/tests/schemas/reflexive.rs
new file mode 100644
index 0000000..02267de
--- /dev/null
+++ b/typify/tests/schemas/reflexive.rs
@@ -0,0 +1,15 @@
+#[allow(unused_imports)]
+use serde::{Deserialize, Serialize};
+#[derive(Clone, Debug, Deserialize, Serialize)]
+pub struct Node {
+    #[serde(default, skip_serializing_if = "Vec::is_empty")]
+    pub children: Vec<Node>,
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub value: Option<i64>,
+}
+impl From<&Node> for Node {
+    fn from(value: &Node) -> Self {
+        value.clone()
+    }
+}
+fn main() {}