Repository: oxidecomputer/typify
Pull request #226 (2023-03-17 02:57:54)

use serde_json::Map with serde_json::Value
See https://github.com/oxidecomputer/progenitor/issues/361


Patch:
diff --git a/typify-impl/src/convert.rs b/typify-impl/src/convert.rs
index fffff96..938983b 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 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()
+                                },
+                            ),
                         };
-                        let details = match maybe_ty {
-                            Some(ty) => VariantDetails::Item(self.assign_type(ty)),
-                            None => VariantDetails::Simple,
-                        };
-                        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)) => {
@@ -1353,8 +1413,8 @@ impl TypeSpace {
         &mut self,
         metadata: &'a Option<Box<Metadata>>,
     ) -> Result<(TypeEntry, &'a Option<Box<Metadata>>)> {
-        let any = TypeEntry::new_native("serde_json::Value", &[]);
-        let type_id = self.assign_type(any);
+        self.uses_serde_json = true;
+        let type_id = self.assign_type(TypeEntryDetails::JsonValue.into());
         Ok((TypeEntryDetails::Vec(type_id).into(), metadata))
     }
 
@@ -1371,7 +1431,7 @@ impl TypeSpace {
         metadata: &'a Option<Box<Metadata>>,
     ) -> Result<(TypeEntry, &'a Option<Box<Metadata>>)> {
         self.uses_serde_json = true;
-        Ok((TypeEntry::new_native("serde_json::Value", &[]), metadata))
+        Ok((TypeEntryDetails::JsonValue.into(), metadata))
     }
 
     fn convert_typed_enum<'a>(
@@ -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/defaults.rs b/typify-impl/src/defaults.rs
index 0216ecd..cdb23cc 100644
--- a/typify-impl/src/defaults.rs
+++ b/typify-impl/src/defaults.rs
@@ -265,6 +265,7 @@ impl TypeEntry {
                 // this Value is not valid for this built-in type.
                 Ok(DefaultKind::Specific)
             }
+            TypeEntryDetails::JsonValue => Ok(DefaultKind::Specific),
             TypeEntryDetails::Boolean => match default {
                 serde_json::Value::Bool(false) => Ok(DefaultKind::Intrinsic),
                 serde_json::Value::Bool(true) => Ok(DefaultKind::Generic(DefaultImpl::Boolean)),
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..e8bad17 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)
         }
@@ -887,6 +926,7 @@ impl<'a> Type<'a> {
             | TypeEntryDetails::Float(name) => TypeDetails::Builtin(name.as_str()),
             TypeEntryDetails::Boolean => TypeDetails::Builtin("bool"),
             TypeEntryDetails::String => TypeDetails::String,
+            TypeEntryDetails::JsonValue => TypeDetails::Builtin("serde_json::Value"),
 
             // Only used during processing; shouldn't be visible at this point
             TypeEntryDetails::Reference(_) => unreachable!(),
diff --git a/typify-impl/src/structs.rs b/typify-impl/src/structs.rs
index 51cafd8..a5ff34b 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
@@ -496,10 +493,29 @@ pub(crate) fn generate_serde_attr(
             serde_options.push(quote! { skip_serializing_if = "Vec::is_empty" });
             DefaultFunction::Default
         }
-        (StructPropertyState::Optional, TypeEntryDetails::Map(..)) => {
+        (StructPropertyState::Optional, TypeEntryDetails::Map(key_id, value_id)) => {
             serde_options.push(quote! { default });
-            serde_options
-                .push(quote! { skip_serializing_if = "std::collections::HashMap::is_empty" });
+
+            let key_ty = type_space
+                .id_to_entry
+                .get(key_id)
+                .expect("unresolved key type id for map");
+            let value_ty = type_space
+                .id_to_entry
+                .get(value_id)
+                .expect("unresolved value type id for map");
+
+            if key_ty.details == TypeEntryDetails::String
+                && value_ty.details == TypeEntryDetails::JsonValue
+            {
+                serde_options.push(quote! {
+                    skip_serializing_if = "serde_json::Map::is_empty"
+                });
+            } else {
+                serde_options.push(quote! {
+                    skip_serializing_if = "std::collections::HashMap::is_empty"
+                });
+            }
             DefaultFunction::Default
         }
         (StructPropertyState::Optional, _) => {
@@ -639,7 +655,7 @@ mod tests {
     #[derive(Serialize, JsonSchema, Schema)]
     struct SomeMaps {
         strings: std::collections::HashMap<String, String>,
-        things: std::collections::HashMap<String, serde_json::Value>,
+        things: serde_json::Map<String, serde_json::Value>,
     }
 
     #[test]
@@ -684,9 +700,6 @@ mod tests {
         let mut type_space = TypeSpace::default();
         let (ty, _) = type_space.convert_schema(Name::Unknown, &schema).unwrap();
         let output = ty.type_name(&type_space).replace(" ", "");
-        assert_eq!(
-            output,
-            "std::collections::HashMap<String,serde_json::Value>"
-        );
+        assert_eq!(output, "serde_json::Map<String,serde_json::Value>");
     }
 }
diff --git a/typify-impl/src/type_entry.rs b/typify-impl/src/type_entry.rs
index a006c16..0c56baa 100644
--- a/typify-impl/src/type_entry.rs
+++ b/typify-impl/src/type_entry.rs
@@ -128,6 +128,8 @@ pub(crate) enum TypeEntryDetails {
     Float(String),
     /// Strings... which we handle a little specially.
     String,
+    /// serde_json::Value which we also handle specially.
+    JsonValue,
 
     /// While these types won't very make their way out to the user, we need
     /// reference types in particular to represent simple type aliases between
@@ -534,6 +536,8 @@ impl TypeEntry {
                 }
             }
 
+            TypeEntryDetails::JsonValue => false,
+
             TypeEntryDetails::Unit
             | TypeEntryDetails::Option(_)
             | TypeEntryDetails::Vec(_)
@@ -1551,15 +1555,21 @@ impl TypeEntry {
                 let key_ty = type_space
                     .id_to_entry
                     .get(key_id)
-                    .expect("unresolved type id for map key")
-                    .type_ident(type_space, type_mod);
+                    .expect("unresolved type id for map key");
                 let value_ty = type_space
                     .id_to_entry
                     .get(value_id)
-                    .expect("unresolved type id for map valuevalue_id")
-                    .type_ident(type_space, type_mod);
+                    .expect("unresolved type id for map value");
 
-                quote! { std::collections::HashMap<#key_ty, #value_ty> }
+                if key_ty.details == TypeEntryDetails::String
+                    && value_ty.details == TypeEntryDetails::JsonValue
+                {
+                    quote! { serde_json::Map<String, serde_json::Value> }
+                } else {
+                    let key_ident = key_ty.type_ident(type_space, type_mod);
+                    let value_ident = value_ty.type_ident(type_space, type_mod);
+                    quote! { std::collections::HashMap<#key_ident, #value_ident> }
+                }
             }
 
             TypeEntryDetails::Set(id) => {
@@ -1603,6 +1613,7 @@ impl TypeEntry {
             TypeEntryDetails::Unit => quote! { () },
             TypeEntryDetails::String => quote! { String },
             TypeEntryDetails::Boolean => quote! { bool },
+            TypeEntryDetails::JsonValue => quote! { serde_json::Value },
             TypeEntryDetails::Native(TypeEntryNative {
                 type_name: name, ..
             })
@@ -1637,11 +1648,10 @@ impl TypeEntry {
             TypeEntryDetails::Enum(TypeEntryEnum { variants, .. })
                 if variants
                     .iter()
-                    .all(|variant| matches!(variant.details, VariantDetails::Simple)) =>
+                    .all(|variant| matches!(&variant.details, VariantDetails::Simple)) =>
             {
                 self.type_ident(type_space, &type_space.settings.type_mod)
             }
-
             TypeEntryDetails::Enum(_)
             | TypeEntryDetails::Struct(_)
             | TypeEntryDetails::Newtype(_)
@@ -1650,7 +1660,8 @@ impl TypeEntry {
             | TypeEntryDetails::Set(_)
             | TypeEntryDetails::Box(_)
             | TypeEntryDetails::Native(_)
-            | TypeEntryDetails::Array(..) => {
+            | TypeEntryDetails::Array(..)
+            | TypeEntryDetails::JsonValue => {
                 let ident = self.type_ident(type_space, &type_space.settings.type_mod);
                 quote! {
                     & #lifetime #ident
@@ -1671,6 +1682,7 @@ impl TypeEntry {
                     _ => quote! { Option<#inner_ident> },
                 }
             }
+
             TypeEntryDetails::Tuple(items) => {
                 let type_streams = items.iter().map(|item| {
                     type_space
@@ -1739,6 +1751,8 @@ impl TypeEntry {
             | TypeEntryDetails::Float(name) => name.clone(),
             TypeEntryDetails::String => "string".to_string(),
 
+            TypeEntryDetails::JsonValue => "json value".to_string(),
+
             TypeEntryDetails::Reference(_) => unreachable!(),
         }
     }
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,
diff --git a/typify-impl/src/value.rs b/typify-impl/src/value.rs
index f2bd3d5..a9b9b7a 100644
--- a/typify-impl/src/value.rs
+++ b/typify-impl/src/value.rs
@@ -150,6 +150,12 @@ impl TypeEntry {
                     serde_json::from_str::< #( #type_path )::* >(#text).unwrap()
                 }
             }
+            TypeEntryDetails::JsonValue => {
+                let text = value.to_string();
+                quote! {
+                    serde_json::from_str::<serde_json::Value>(#text).unwrap()
+                }
+            }
             TypeEntryDetails::Boolean => {
                 let v = value.as_bool()?;
                 quote! { #v }


Test patch:
diff --git a/cargo-typify/tests/outputs/builder.rs b/cargo-typify/tests/outputs/builder.rs
index 942b96f..9a39fad 100644
--- a/cargo-typify/tests/outputs/builder.rs
+++ b/cargo-typify/tests/outputs/builder.rs
@@ -6,14 +6,14 @@
 use serde::{Deserialize, Serialize};
 
 #[derive(Clone, Debug, Deserialize, Serialize)]
-pub struct Fruit(pub std::collections::HashMap<String, serde_json::Value>);
+pub struct Fruit(pub serde_json::Map<String, serde_json::Value>);
 impl std::ops::Deref for Fruit {
-    type Target = std::collections::HashMap<String, serde_json::Value>;
-    fn deref(&self) -> &std::collections::HashMap<String, serde_json::Value> {
+    type Target = serde_json::Map<String, serde_json::Value>;
+    fn deref(&self) -> &serde_json::Map<String, serde_json::Value> {
         &self.0
     }
 }
-impl From<Fruit> for std::collections::HashMap<String, serde_json::Value> {
+impl From<Fruit> for serde_json::Map<String, serde_json::Value> {
     fn from(value: Fruit) -> Self {
         value.0
     }
@@ -23,8 +23,8 @@ impl From<&Fruit> for Fruit {
         value.clone()
     }
 }
-impl From<std::collections::HashMap<String, serde_json::Value>> for Fruit {
-    fn from(value: std::collections::HashMap<String, serde_json::Value>) -> Self {
+impl From<serde_json::Map<String, serde_json::Value>> for Fruit {
+    fn from(value: serde_json::Map<String, serde_json::Value>) -> Self {
         Self(value)
     }
 }
diff --git a/cargo-typify/tests/outputs/derive.rs b/cargo-typify/tests/outputs/derive.rs
index b6b756a..ec3f7a8 100644
--- a/cargo-typify/tests/outputs/derive.rs
+++ b/cargo-typify/tests/outputs/derive.rs
@@ -6,14 +6,14 @@
 use serde::{Deserialize, Serialize};
 
 #[derive(Clone, Debug, Deserialize, ExtraDerive, Serialize)]
-pub struct Fruit(pub std::collections::HashMap<String, serde_json::Value>);
+pub struct Fruit(pub serde_json::Map<String, serde_json::Value>);
 impl std::ops::Deref for Fruit {
-    type Target = std::collections::HashMap<String, serde_json::Value>;
-    fn deref(&self) -> &std::collections::HashMap<String, serde_json::Value> {
+    type Target = serde_json::Map<String, serde_json::Value>;
+    fn deref(&self) -> &serde_json::Map<String, serde_json::Value> {
         &self.0
     }
 }
-impl From<Fruit> for std::collections::HashMap<String, serde_json::Value> {
+impl From<Fruit> for serde_json::Map<String, serde_json::Value> {
     fn from(value: Fruit) -> Self {
         value.0
     }
@@ -23,8 +23,8 @@ impl From<&Fruit> for Fruit {
         value.clone()
     }
 }
-impl From<std::collections::HashMap<String, serde_json::Value>> for Fruit {
-    fn from(value: std::collections::HashMap<String, serde_json::Value>) -> Self {
+impl From<serde_json::Map<String, serde_json::Value>> for Fruit {
+    fn from(value: serde_json::Map<String, serde_json::Value>) -> Self {
         Self(value)
     }
 }
diff --git a/cargo-typify/tests/outputs/multi_derive.rs b/cargo-typify/tests/outputs/multi_derive.rs
index 6470917..711304f 100644
--- a/cargo-typify/tests/outputs/multi_derive.rs
+++ b/cargo-typify/tests/outputs/multi_derive.rs
@@ -6,14 +6,14 @@
 use serde::{Deserialize, Serialize};
 
 #[derive(AnotherDerive, Clone, Debug, Deserialize, ExtraDerive, Serialize)]
-pub struct Fruit(pub std::collections::HashMap<String, serde_json::Value>);
+pub struct Fruit(pub serde_json::Map<String, serde_json::Value>);
 impl std::ops::Deref for Fruit {
-    type Target = std::collections::HashMap<String, serde_json::Value>;
-    fn deref(&self) -> &std::collections::HashMap<String, serde_json::Value> {
+    type Target = serde_json::Map<String, serde_json::Value>;
+    fn deref(&self) -> &serde_json::Map<String, serde_json::Value> {
         &self.0
     }
 }
-impl From<Fruit> for std::collections::HashMap<String, serde_json::Value> {
+impl From<Fruit> for serde_json::Map<String, serde_json::Value> {
     fn from(value: Fruit) -> Self {
         value.0
     }
@@ -23,8 +23,8 @@ impl From<&Fruit> for Fruit {
         value.clone()
     }
 }
-impl From<std::collections::HashMap<String, serde_json::Value>> for Fruit {
-    fn from(value: std::collections::HashMap<String, serde_json::Value>) -> Self {
+impl From<serde_json::Map<String, serde_json::Value>> for Fruit {
+    fn from(value: serde_json::Map<String, serde_json::Value>) -> Self {
         Self(value)
     }
 }
diff --git a/cargo-typify/tests/outputs/no-builder.rs b/cargo-typify/tests/outputs/no-builder.rs
index 9dc833e..78d133c 100644
--- a/cargo-typify/tests/outputs/no-builder.rs
+++ b/cargo-typify/tests/outputs/no-builder.rs
@@ -6,14 +6,14 @@
 use serde::{Deserialize, Serialize};
 
 #[derive(Clone, Debug, Deserialize, Serialize)]
-pub struct Fruit(pub std::collections::HashMap<String, serde_json::Value>);
+pub struct Fruit(pub serde_json::Map<String, serde_json::Value>);
 impl std::ops::Deref for Fruit {
-    type Target = std::collections::HashMap<String, serde_json::Value>;
-    fn deref(&self) -> &std::collections::HashMap<String, serde_json::Value> {
+    type Target = serde_json::Map<String, serde_json::Value>;
+    fn deref(&self) -> &serde_json::Map<String, serde_json::Value> {
         &self.0
     }
 }
-impl From<Fruit> for std::collections::HashMap<String, serde_json::Value> {
+impl From<Fruit> for serde_json::Map<String, serde_json::Value> {
     fn from(value: Fruit) -> Self {
         value.0
     }
@@ -23,8 +23,8 @@ impl From<&Fruit> for Fruit {
         value.clone()
     }
 }
-impl From<std::collections::HashMap<String, serde_json::Value>> for Fruit {
-    fn from(value: std::collections::HashMap<String, serde_json::Value>) -> Self {
+impl From<serde_json::Map<String, serde_json::Value>> for Fruit {
+    fn from(value: serde_json::Map<String, serde_json::Value>) -> Self {
         Self(value)
     }
 }
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-impl/tests/github.out b/typify-impl/tests/github.out
index 8afc5bf..5a5c1e9 100644
--- a/typify-impl/tests/github.out
+++ b/typify-impl/tests/github.out
@@ -22843,7 +22843,7 @@ impl From<RepositoryDispatchOnDemandTest> for RepositoryDispatchEvent {
 pub struct RepositoryDispatchOnDemandTest {
     pub action: RepositoryDispatchOnDemandTestAction,
     pub branch: String,
-    pub client_payload: std::collections::HashMap<String, serde_json::Value>,
+    pub client_payload: serde_json::Map<String, serde_json::Value>,
     pub installation: InstallationLite,
     #[serde(default, skip_serializing_if = "Option::is_none")]
     pub organization: Option<Organization>,
@@ -27170,7 +27170,7 @@ impl From<&Workflow> for Workflow {
 #[derive(Clone, Debug, Deserialize, Serialize)]
 #[serde(deny_unknown_fields)]
 pub struct WorkflowDispatchEvent {
-    pub inputs: Option<std::collections::HashMap<String, serde_json::Value>>,
+    pub inputs: Option<serde_json::Map<String, serde_json::Value>>,
     #[serde(default, skip_serializing_if = "Option::is_none")]
     pub installation: Option<InstallationLite>,
     #[serde(default, skip_serializing_if = "Option::is_none")]
diff --git a/typify-impl/tests/vega.out b/typify-impl/tests/vega.out
index 64fce4d..6d831c9 100644
--- a/typify-impl/tests/vega.out
+++ b/typify-impl/tests/vega.out
@@ -12128,8 +12128,8 @@ pub struct EverythingSubtype1 {
     pub autosize: Option<Autosize>,
     #[serde(default, skip_serializing_if = "Option::is_none")]
     pub background: Option<Background>,
-    #[serde(default, skip_serializing_if = "std::collections::HashMap::is_empty")]
-    pub config: std::collections::HashMap<String, serde_json::Value>,
+    #[serde(default, skip_serializing_if = "serde_json::Map::is_empty")]
+    pub config: serde_json::Map<String, serde_json::Value>,
     #[serde(default, skip_serializing_if = "Option::is_none")]
     pub description: Option<String>,
     #[serde(default, skip_serializing_if = "Option::is_none")]
@@ -24457,7 +24457,7 @@ impl From<SignalRef> for ProjectionExtentVariant0Item {
 #[derive(Clone, Debug, Deserialize, Serialize)]
 #[serde(untagged)]
 pub enum ProjectionFit {
-    Variant0(std::collections::HashMap<String, serde_json::Value>),
+    Variant0(serde_json::Map<String, serde_json::Value>),
     Variant1(Vec<serde_json::Value>),
 }
 impl From<&ProjectionFit> for ProjectionFit {
@@ -24465,8 +24465,8 @@ impl From<&ProjectionFit> for ProjectionFit {
         value.clone()
     }
 }
-impl From<std::collections::HashMap<String, serde_json::Value>> for ProjectionFit {
-    fn from(value: std::collections::HashMap<String, serde_json::Value>) -> Self {
+impl From<serde_json::Map<String, serde_json::Value>> for ProjectionFit {
+    fn from(value: serde_json::Map<String, serde_json::Value>) -> Self {
         Self::Variant0(value)
     }
 }
@@ -30595,8 +30595,8 @@ pub struct Scope {
     pub signals: Vec<Signal>,
     #[serde(default, skip_serializing_if = "Option::is_none")]
     pub title: Option<Title>,
-    #[serde(default, skip_serializing_if = "std::collections::HashMap::is_empty")]
-    pub usermeta: std::collections::HashMap<String, serde_json::Value>,
+    #[serde(default, skip_serializing_if = "serde_json::Map::is_empty")]
+    pub usermeta: serde_json::Map<String, serde_json::Value>,
 }
 impl From<&Scope> for Scope {
     fn from(value: &Scope) -> Self {
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..ba7c9fa 100644
--- a/typify/tests/schemas/multiple-instance-types.rs
+++ b/typify/tests/schemas/multiple-instance-types.rs
@@ -59,7 +59,7 @@ impl From<i64> for IntOrStr {
 pub enum OneOfSeveral {
     Null,
     Boolean(bool),
-    Object(std::collections::HashMap<String, serde_json::Value>),
+    Object(serde_json::Map<String, serde_json::Value>),
     Array(Vec<serde_json::Value>),
     String(String),
     Integer(i64),
@@ -74,8 +74,8 @@ impl From<bool> for OneOfSeveral {
         Self::Boolean(value)
     }
 }
-impl From<std::collections::HashMap<String, serde_json::Value>> for OneOfSeveral {
-    fn from(value: std::collections::HashMap<String, serde_json::Value>) -> Self {
+impl From<serde_json::Map<String, serde_json::Value>> for OneOfSeveral {
+    fn from(value: serde_json::Map<String, serde_json::Value>) -> Self {
         Self::Object(value)
     }
 }
@@ -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() {}