Repository: cedar-policy/cedar
Pull request #1406 (2025-01-03 15:42:23)

propagate source locations on cedar syntax types
## Description of changes

Continues addressing #1084, by propagating source locations on `Type`s.  See also #1405, which did this for some other structures.

## Issue #, if available

Partially addresses #1084

## Checklist for requesting a review

The change in this PR is (choose one, and delete the other options):

- [x] A change "invisible" to users (e.g., documentation, changes to "internal" crates like `cedar-policy-core`, `cedar-validator`, etc.)

I confirm that this PR (choose one, and delete the other options):

- [x] Does not update the CHANGELOG because my change does not significantly impact released code.

I confirm that [`cedar-spec`](https://github.com/cedar-policy/cedar-spec) (choose one, and delete the other options):

- [x] Requires updates, and I have made / will make these updates myself. (Please include in your description a timeline or link to the relevant PR in `cedar-spec`, and how you have tested that your updates are correct.)

I confirm that [`docs.cedarpolicy.com`](https://docs.cedarpolicy.com/) (choose one, and delete the other options):

- [x] Does not require updates because my change does not impact the Cedar language specification.



Patch:
diff --git a/cedar-policy-validator/src/cedar_schema/ast.rs b/cedar-policy-validator/src/cedar_schema/ast.rs
index 9279c28a..e93ddce1 100644
--- a/cedar-policy-validator/src/cedar_schema/ast.rs
+++ b/cedar-policy-validator/src/cedar_schema/ast.rs
@@ -258,7 +258,7 @@ pub struct EntityDecl {
     /// Entity Types this type is allowed to be related to via the `in` relation
     pub member_of_types: Vec<Path>,
     /// Attributes this entity has
-    pub attrs: Vec<Node<Annotated<AttrDecl>>>,
+    pub attrs: Node<Vec<Node<Annotated<AttrDecl>>>>,
     /// Tag type for this entity (`None` means no tags on this entity)
     pub tags: Option<Node<Type>>,
 }
@@ -340,7 +340,7 @@ pub enum AppDecl {
     /// Constraints on the `principal` or `resource`
     PR(PRAppDecl),
     /// Constraints on the `context`
-    Context(Either<Path, Vec<Node<Annotated<AttrDecl>>>>),
+    Context(Either<Path, Node<Vec<Node<Annotated<AttrDecl>>>>>),
 }
 
 /// An action declaration
diff --git a/cedar-policy-validator/src/cedar_schema/fmt.rs b/cedar-policy-validator/src/cedar_schema/fmt.rs
index f559f66a..8a37099c 100644
--- a/cedar-policy-validator/src/cedar_schema/fmt.rs
+++ b/cedar-policy-validator/src/cedar_schema/fmt.rs
@@ -57,7 +57,7 @@ impl<N: Display> Display for json_schema::NamespaceDefinition<N> {
 impl<N: Display> Display for json_schema::Type<N> {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
         match self {
-            json_schema::Type::Type(ty) => match ty {
+            json_schema::Type::Type { ty, .. } => match ty {
                 json_schema::TypeVariant::Boolean => write!(f, "__cedar::Bool"),
                 json_schema::TypeVariant::Entity { name } => write!(f, "{name}"),
                 json_schema::TypeVariant::EntityOrCommon { type_name } => {
@@ -69,7 +69,7 @@ impl<N: Display> Display for json_schema::Type<N> {
                 json_schema::TypeVariant::Set { element } => write!(f, "Set < {element} >"),
                 json_schema::TypeVariant::String => write!(f, "__cedar::String"),
             },
-            json_schema::Type::CommonTypeRef { type_name } => write!(f, "{type_name}"),
+            json_schema::Type::CommonTypeRef { type_name, .. } => write!(f, "{type_name}"),
         }
     }
 }
diff --git a/cedar-policy-validator/src/cedar_schema/grammar.lalrpop b/cedar-policy-validator/src/cedar_schema/grammar.lalrpop
index 91052835..864836f5 100644
--- a/cedar-policy-validator/src/cedar_schema/grammar.lalrpop
+++ b/cedar-policy-validator/src/cedar_schema/grammar.lalrpop
@@ -125,10 +125,15 @@ Decl: Node<Declaration> = {
     <t:TypeDecl> => t,
 }
 
-// Entity := 'entity' Idents ['in' EntOrTypes] [['='] RecType] ';'
+// Entity := 'entity' Idents ['in' EntOrTypes] [['='] RecType] ';' <r:@R>
 Entity: Node<Declaration> = {
-    <l:@L> ENTITY <ets: Idents> <ps:(IN <EntTypes>)?> <ds:("="? "{" <AttrDecls?> "}")?> <ts:(TAGS <Type>)?> ";" <r:@R>
-        => Node::with_source_loc(Declaration::Entity(EntityDecl { names: ets, member_of_types: ps.unwrap_or_default(), attrs: ds.map(|ds| ds.unwrap_or_default()).unwrap_or_default(), tags: ts }), Loc::new(l..r, Arc::clone(src))),
+    <l1:@L> ENTITY <ets: Idents> <ps:(IN <EntTypes>)?> <l2:@L> <ds:("="? "{" <AttrDecls?> "}")?> <r2:@R> <ts:(TAGS <Type>)?> ";" <r1:@R>
+        => Node::with_source_loc(Declaration::Entity(EntityDecl {
+            names: ets,
+            member_of_types: ps.unwrap_or_default(),
+            attrs: Node::with_source_loc(ds.map(|ds| ds.unwrap_or_default()).unwrap_or_default(), Loc::new(l2..r2, Arc::clone(src))),
+            tags: ts,
+            }), Loc::new(l1..r1, Arc::clone(src))),
 }
 
 // Action := 'action' Names ['in' QualNameOrNames]
@@ -175,18 +180,18 @@ AppDecls: Node<NonEmpty<Node<AppDecl>>> = {
                 ds,
                 Loc::new(l..r, Arc::clone(src)))
         },
-    <l:@L> CONTEXT ":" "{" <attrs:AttrDecls?> "}" ","? <r:@R>
+    <l1:@L> CONTEXT ":" <l2:@L> "{" <attrs:AttrDecls?> "}" <r2:@R> ","? <r1:@R>
         =>
             Node::with_source_loc(
-                nonempty![Node::with_source_loc(AppDecl::Context(Either::Right(attrs.unwrap_or_default())), Loc::new(l..r, Arc::clone(src)))],
-                Loc::new(l..r, Arc::clone(src))),
-    <l:@L> CONTEXT ":" "{" <attrs:AttrDecls?> "}" "," <r:@R> <mut ds: AppDecls>
+                nonempty![Node::with_source_loc(AppDecl::Context(Either::Right(Node::with_source_loc(attrs.unwrap_or_default(), Loc::new(l2..r2, Arc::clone(src))))), Loc::new(l1..r1, Arc::clone(src)))],
+                Loc::new(l1..r1, Arc::clone(src))),
+    <l1:@L> CONTEXT ":" <l2:@L> "{" <attrs:AttrDecls?> "}" <r2:@R> "," <r1:@R> <mut ds: AppDecls>
         => {
             let (mut ds, _) = ds.into_inner();
-            ds.insert(0, Node::with_source_loc(AppDecl::Context(Either::Right(attrs.unwrap_or_default())), Loc::new(l..r, Arc::clone(src))));
+            ds.insert(0, Node::with_source_loc(AppDecl::Context(Either::Right(Node::with_source_loc(attrs.unwrap_or_default(), Loc::new(l2..r2, Arc::clone(src))))), Loc::new(l1..r1, Arc::clone(src))));
             Node::with_source_loc(
                 ds,
-                Loc::new(l..r, Arc::clone(src)))
+                Loc::new(l1..r1, Arc::clone(src)))
         },
 }
 
diff --git a/cedar-policy-validator/src/cedar_schema/to_json_schema.rs b/cedar-policy-validator/src/cedar_schema/to_json_schema.rs
index 661949b8..882c6603 100644
--- a/cedar-policy-validator/src/cedar_schema/to_json_schema.rs
+++ b/cedar-policy-validator/src/cedar_schema/to_json_schema.rs
@@ -98,22 +98,21 @@ fn is_valid_ext_type(ty: &Id, extensions: &Extensions<'_>) -> bool {
 
 /// Convert a `Type` into the JSON representation of the type.
 pub fn cedar_type_to_json_type(ty: Node<Type>) -> json_schema::Type<RawName> {
-    match ty.node {
-        Type::Set(t) => json_schema::Type::Type(json_schema::TypeVariant::Set {
+    let variant = match ty.node {
+        Type::Set(t) => json_schema::TypeVariant::Set {
             element: Box::new(cedar_type_to_json_type(*t)),
-        }),
-        Type::Ident(p) => json_schema::Type::Type(json_schema::TypeVariant::EntityOrCommon {
+        },
+        Type::Ident(p) => json_schema::TypeVariant::EntityOrCommon {
             type_name: RawName::from(p),
+        },
+        Type::Record(fields) => json_schema::TypeVariant::Record(json_schema::RecordType {
+            attributes: fields.into_iter().map(convert_attr_decl).collect(),
+            additional_attributes: false,
         }),
-        Type::Record(fields) => {
-            json_schema::Type::Type(json_schema::TypeVariant::Record(json_schema::RecordType {
-                attributes: fields
-                    .into_iter()
-                    .map(|field| convert_attr_decl(field.node))
-                    .collect(),
-                additional_attributes: false,
-            }))
-        }
+    };
+    json_schema::Type::Type {
+        ty: variant,
+        loc: Some(ty.loc),
     }
 }
 
@@ -386,48 +385,46 @@ fn convert_entity_decl(
 
 /// Create a [`json_schema::AttributesOrContext`] from a series of `AttrDecl`s
 fn convert_attr_decls(
-    attrs: impl IntoIterator<Item = Node<Annotated<AttrDecl>>>,
+    attrs: Node<impl IntoIterator<Item = Node<Annotated<AttrDecl>>>>,
 ) -> json_schema::AttributesOrContext<RawName> {
-    json_schema::RecordType {
-        attributes: attrs
-            .into_iter()
-            .map(|attr| convert_attr_decl(attr.node))
-            .collect(),
-        additional_attributes: false,
-    }
-    .into()
+    json_schema::AttributesOrContext(json_schema::Type::Type {
+        ty: json_schema::TypeVariant::Record(json_schema::RecordType {
+            attributes: attrs.node.into_iter().map(convert_attr_decl).collect(),
+            additional_attributes: false,
+        }),
+        loc: Some(attrs.loc),
+    })
 }
 
 /// Create a context decl
 fn convert_context_decl(
-    decl: Either<Path, Vec<Node<Annotated<AttrDecl>>>>,
+    decl: Either<Path, Node<Vec<Node<Annotated<AttrDecl>>>>>,
 ) -> json_schema::AttributesOrContext<RawName> {
     json_schema::AttributesOrContext(match decl {
         Either::Left(p) => json_schema::Type::CommonTypeRef {
+            loc: Some(p.loc().clone()),
             type_name: p.into(),
         },
-        Either::Right(attrs) => {
-            json_schema::Type::Type(json_schema::TypeVariant::Record(json_schema::RecordType {
-                attributes: attrs
-                    .into_iter()
-                    .map(|attr| convert_attr_decl(attr.node))
-                    .collect(),
+        Either::Right(attrs) => json_schema::Type::Type {
+            ty: json_schema::TypeVariant::Record(json_schema::RecordType {
+                attributes: attrs.node.into_iter().map(convert_attr_decl).collect(),
                 additional_attributes: false,
-            }))
-        }
+            }),
+            loc: Some(attrs.loc),
+        },
     })
 }
 
 /// Convert an attribute type from an `AttrDecl`
 fn convert_attr_decl(
-    attr: Annotated<AttrDecl>,
+    attr: Node<Annotated<AttrDecl>>,
 ) -> (SmolStr, json_schema::TypeOfAttribute<RawName>) {
     (
-        attr.data.name.node,
+        attr.node.data.name.node,
         json_schema::TypeOfAttribute {
-            ty: cedar_type_to_json_type(attr.data.ty),
-            required: attr.data.required,
-            annotations: attr.annotations.into(),
+            ty: cedar_type_to_json_type(attr.node.data.ty),
+            required: attr.node.data.required,
+            annotations: attr.node.annotations.into(),
         },
     )
 }
@@ -760,4 +757,110 @@ mod preserves_source_locations {
             Some("action List in Read appliesTo {\n                principal: [A],\n                resource: [B, C],\n                context: {\n                    s: Set<S>,\n                    ab: { a: AA, b: B },\n                }\n            };")
         ));
     }
+
+    #[test]
+    fn types() {
+        let (schema, _) = json_schema::Fragment::from_cedarschema_str(
+            r#"
+        namespace NS {
+            type S = String;
+            entity A;
+            entity B in A;
+            entity C in A {
+                bool: Bool,
+                s: S,
+                a: Set<A>,
+                b: { inner: B },
+            };
+            type AA = A;
+            action Read, Write;
+            action List in Read appliesTo {
+                principal: [A],
+                resource: [B, C],
+                context: {
+                    s: Set<S>,
+                    ab: { a: AA, b: B },
+                }
+            };
+        }
+        "#,
+            &Extensions::all_available(),
+        )
+        .unwrap();
+        let ns = schema
+            .0
+            .get(&Some(Name::parse_unqualified_name("NS").unwrap()))
+            .expect("couldn't find namespace NS");
+
+        let entityC = ns
+            .entity_types
+            .get(&"C".parse().unwrap())
+            .expect("couldn't find entity C");
+        assert_matches!(entityC.member_of_types.first().unwrap().loc(), Some(loc) => {
+            assert_matches!(loc.snippet(), Some("A"));
+        });
+        assert_matches!(entityC.shape.0.loc(), Some(loc) => {
+            assert_matches!(loc.snippet(), Some("{\n                bool: Bool,\n                s: S,\n                a: Set<A>,\n                b: { inner: B },\n            }"));
+        });
+        assert_matches!(&entityC.shape.0, json_schema::Type::Type { ty: json_schema::TypeVariant::Record(rty), .. } => {
+            let b = rty.attributes.get("bool").expect("couldn't find attribute `bool` on entity C");
+            assert_matches!(b.ty.loc(), Some(loc) => {
+                assert_matches!(loc.snippet(), Some("Bool"));
+            });
+            let s = rty.attributes.get("s").expect("couldn't find attribute `s` on entity C");
+            assert_matches!(s.ty.loc(), Some(loc) => {
+                assert_matches!(loc.snippet(), Some("S"));
+            });
+            let a = rty.attributes.get("a").expect("couldn't find attribute `a` on entity C");
+            assert_matches!(a.ty.loc(), Some(loc) => {
+                assert_matches!(loc.snippet(), Some("Set<A>"));
+            });
+            assert_matches!(&a.ty, json_schema::Type::Type { ty: json_schema::TypeVariant::Set { element }, .. } => {
+                assert_matches!(element.loc(), Some(loc) => {
+                    assert_matches!(loc.snippet(), Some("A"));
+                });
+            });
+            let b = rty.attributes.get("b").expect("couldn't find attribute `b` on entity C");
+            assert_matches!(b.ty.loc(), Some(loc) => {
+                assert_matches!(loc.snippet(), Some("{ inner: B }"));
+            });
+            assert_matches!(&b.ty, json_schema::Type::Type { ty: json_schema::TypeVariant::Record(b_rty), .. } => {
+                let inner = b_rty.attributes.get("inner").expect("couldn't find inner attribute");
+                assert_matches!(inner.ty.loc(), Some(loc) => {
+                    assert_matches!(loc.snippet(), Some("B"));
+                });
+            });
+        });
+
+        let ctypeAA = ns
+            .common_types
+            .get(&json_schema::CommonTypeId::new("AA".parse().unwrap()).unwrap())
+            .expect("couldn't find common type AA");
+        assert_matches!(ctypeAA.ty.loc(), Some(loc) => {
+            assert_matches!(loc.snippet(), Some("A"));
+        });
+
+        let actionList = ns.actions.get("List").expect("couldn't find action List");
+        assert_matches!(&actionList.applies_to, Some(appliesto) => {
+            assert_matches!(appliesto.principal_types.first().expect("principal types were empty").loc(), Some(loc) => {
+                assert_matches!(loc.snippet(), Some("A"));
+            });
+            assert_matches!(appliesto.resource_types.first().expect("resource types were empty").loc(), Some(loc) => {
+                assert_matches!(loc.snippet(), Some("B"));
+            });
+            assert_matches!(appliesto.context.loc(), Some(loc) => {
+                assert_matches!(loc.snippet(), Some("{\n                    s: Set<S>,\n                    ab: { a: AA, b: B },\n                }"));
+            });
+            assert_matches!(&appliesto.context.0, json_schema::Type::Type { ty: json_schema::TypeVariant::Record(rty), .. } => {
+                let s = rty.attributes.get("s").expect("couldn't find attribute `s` on context");
+                assert_matches!(s.ty.loc(), Some(loc) => {
+                    assert_matches!(loc.snippet(), Some("Set<S>"));
+                });
+                let ab = rty.attributes.get("ab").expect("couldn't find attribute `ab` on context");
+                assert_matches!(ab.ty.loc(), Some(loc) => {
+                    assert_matches!(loc.snippet(), Some("{ a: AA, b: B }"));
+                });
+            });
+        });
+    }
 }
diff --git a/cedar-policy-validator/src/json_schema.rs b/cedar-policy-validator/src/json_schema.rs
index b2017ce0..b9057a34 100644
--- a/cedar-policy-validator/src/json_schema.rs
+++ b/cedar-policy-validator/src/json_schema.rs
@@ -90,7 +90,8 @@ pub struct CommonType<N> {
 ///     processed, by converting [`RawName`]s into [`ConditionalName`]s
 /// - `N` = [`InternalName`]: a [`Fragment`] in which all names have been
 ///     resolved into fully-qualified [`InternalName`]s
-#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
+#[derive(Educe, Debug, Clone, Deserialize)]
+#[educe(PartialEq, Eq)]
 #[serde(bound(deserialize = "N: Deserialize<'de> + From<RawName>"))]
 #[serde(transparent)]
 #[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
@@ -204,7 +205,8 @@ impl<N: Display> Fragment<N> {
 
 /// An [`UnreservedId`] that cannot be reserved JSON schema keywords
 /// like `Set`, `Long`, and etc.
-#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, PartialOrd, Ord)]
+#[derive(Educe, Debug, Clone, Serialize)]
+#[educe(PartialEq, Eq, PartialOrd, Ord, Hash)]
 #[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
 #[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
 pub struct CommonTypeId(#[cfg_attr(feature = "wasm", tsify(type = "string"))] UnreservedId);
@@ -310,7 +312,8 @@ pub struct ReservedCommonTypeBasenameError {
 /// _that are being declared here_, which is always an `UnreservedId` and unambiguously
 /// refers to the [`InternalName`] with the implicit current/active namespace prepended.)
 /// See notes on [`Fragment`].
-#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+#[derive(Educe, Debug, Clone, Serialize, Deserialize)]
+#[educe(PartialEq, Eq)]
 #[serde_as]
 #[serde(bound(deserialize = "N: Deserialize<'de> + From<RawName>"))]
 #[serde(bound(serialize = "N: Serialize"))]
@@ -335,6 +338,9 @@ pub struct NamespaceDefinition<N> {
 }
 
 impl<N> NamespaceDefinition<N> {
+    /// Create a new [`NamespaceDefinition`] with specified entity types and
+    /// actions, and no common types or annotations
+    #[cfg(test)]
     pub fn new(
         entity_types: impl IntoIterator<Item = (UnreservedId, EntityType<N>)>,
         actions: impl IntoIterator<Item = (SmolStr, ActionType<N>)>,
@@ -524,7 +530,8 @@ impl EntityType<ConditionalName> {
 /// The parameter `N` is the type of entity type names and common type names in
 /// this [`AttributesOrContext`], including recursively.
 /// See notes on [`Fragment`].
-#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+#[derive(Educe, Debug, Clone, Serialize, Deserialize)]
+#[educe(PartialEq, Eq)]
 #[serde(bound(deserialize = "N: Deserialize<'de> + From<RawName>"))]
 #[serde(transparent)]
 #[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
@@ -545,6 +552,11 @@ impl<N> AttributesOrContext<N> {
     pub fn is_empty_record(&self) -> bool {
         self.0.is_empty_record()
     }
+
+    /// Get the source location of this `AttributesOrContext`
+    pub fn loc(&self) -> Option<&Loc> {
+        self.0.loc()
+    }
 }
 
 impl<N> Default for AttributesOrContext<N> {
@@ -561,7 +573,10 @@ impl<N: Display> Display for AttributesOrContext<N> {
 
 impl<N> From<RecordType<N>> for AttributesOrContext<N> {
     fn from(rty: RecordType<N>) -> AttributesOrContext<N> {
-        Self(Type::Type(TypeVariant::Record(rty)))
+        Self(Type::Type {
+            ty: TypeVariant::Record(rty),
+            loc: None,
+        })
     }
 }
 
@@ -697,7 +712,8 @@ impl ActionType<ConditionalName> {
 /// The parameter `N` is the type of entity type names and common type names in
 /// this [`ApplySpec`], including recursively.
 /// See notes on [`Fragment`].
-#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+#[derive(Educe, Debug, Clone, Serialize, Deserialize)]
+#[educe(PartialEq, Eq)]
 #[serde(bound(deserialize = "N: Deserialize<'de> + From<RawName>"))]
 #[serde(deny_unknown_fields)]
 #[serde(rename_all = "camelCase")]
@@ -764,7 +780,8 @@ impl ApplySpec<ConditionalName> {
 }
 
 /// Represents the [`cedar_policy_core::ast::EntityUID`] of an action
-#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+#[derive(Educe, Debug, Clone, Serialize, Deserialize)]
+#[educe(PartialEq, Eq, Hash)]
 #[serde(bound(deserialize = "N: Deserialize<'de> + From<RawName>"))]
 #[serde(deny_unknown_fields)]
 #[serde(rename_all = "camelCase")]
@@ -965,7 +982,8 @@ impl From<EntityUID> for ActionEntityUID<Name> {
 /// The parameter `N` is the type of entity type names and common type names in
 /// this [`Type`], including recursively.
 /// See notes on [`Fragment`].
-#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)]
+#[derive(Educe, Debug, Clone, Serialize)]
+#[educe(PartialEq, Eq, PartialOrd, Ord)]
 // This enum is `untagged` with these variants as a workaround to a serde
 // limitation. It is not possible to have the known variants on one enum, and
 // then, have catch-all variant for any unrecognized tag in the same enum that
@@ -977,7 +995,20 @@ pub enum Type<N> {
     /// One of the standard types exposed to users.
     ///
     /// This branch also includes the "entity-or-common-type-reference" possibility.
-    Type(TypeVariant<N>),
+    Type {
+        /// The type
+        #[serde(flatten)]
+        ty: TypeVariant<N>,
+        /// Source location
+        ///
+        /// (As of this writing, this is not populated when parsing from JSON.
+        /// It is only populated if constructing this structure from the
+        /// corresponding Cedar-syntax structure.)
+        #[serde(skip)]
+        #[educe(PartialEq(ignore))]
+        #[educe(PartialOrd(ignore))]
+        loc: Option<Loc>,
+    },
     /// Reference to a common type
     ///
     /// This is only used for references that _must_ resolve to common types.
@@ -990,6 +1021,15 @@ pub enum Type<N> {
         /// may not yet be fully qualified
         #[serde(rename = "type")]
         type_name: N,
+        /// Source location
+        ///
+        /// (As of this writing, this is not populated when parsing from JSON.
+        /// It is only populated if constructing this structure from the
+        /// corresponding Cedar-syntax structure.)
+        #[serde(skip)]
+        #[educe(PartialEq(ignore))]
+        #[educe(PartialOrd(ignore))]
+        loc: Option<Loc>,
     },
 }
 
@@ -998,17 +1038,24 @@ impl<N> Type<N> {
     /// resolve to a common type
     pub(crate) fn common_type_references(&self) -> Box<dyn Iterator<Item = &N> + '_> {
         match self {
-            Type::Type(TypeVariant::Record(RecordType { attributes, .. })) => attributes
+            Type::Type {
+                ty: TypeVariant::Record(RecordType { attributes, .. }),
+                ..
+            } => attributes
                 .iter()
                 .map(|(_, ty)| ty.ty.common_type_references())
                 .fold(Box::new(std::iter::empty()), |it, tys| {
                     Box::new(it.chain(tys))
                 }),
-            Type::Type(TypeVariant::Set { element }) => element.common_type_references(),
-            Type::Type(TypeVariant::EntityOrCommon { type_name }) => {
-                Box::new(std::iter::once(type_name))
-            }
-            Type::CommonTypeRef { type_name } => Box::new(std::iter::once(type_name)),
+            Type::Type {
+                ty: TypeVariant::Set { element },
+                ..
+            } => element.common_type_references(),
+            Type::Type {
+                ty: TypeVariant::EntityOrCommon { type_name },
+                ..
+            } => Box::new(std::iter::once(type_name)),
+            Type::CommonTypeRef { type_name, .. } => Box::new(std::iter::once(type_name)),
             _ => Box::new(std::iter::empty()),
         }
     }
@@ -1020,16 +1067,25 @@ impl<N> Type<N> {
     /// [`crate::types::Type`].
     pub fn is_extension(&self) -> Option<bool> {
         match self {
-            Self::Type(TypeVariant::Extension { .. }) => Some(true),
-            Self::Type(TypeVariant::Set { element }) => element.is_extension(),
-            Self::Type(TypeVariant::Record(RecordType { attributes, .. })) => attributes
+            Self::Type {
+                ty: TypeVariant::Extension { .. },
+                ..
+            } => Some(true),
+            Self::Type {
+                ty: TypeVariant::Set { element },
+                ..
+            } => element.is_extension(),
+            Self::Type {
+                ty: TypeVariant::Record(RecordType { attributes, .. }),
+                ..
+            } => attributes
                 .values()
                 .try_fold(false, |a, e| match e.ty.is_extension() {
                     Some(true) => Some(true),
                     Some(false) => Some(a),
                     None => None,
                 }),
-            Self::Type(_) => Some(false),
+            Self::Type { .. } => Some(false),
             Self::CommonTypeRef { .. } => None,
         }
     }
@@ -1038,10 +1094,21 @@ impl<N> Type<N> {
     /// implementation to avoid printing unnecessary entity/action data.
     pub fn is_empty_record(&self) -> bool {
         match self {
-            Self::Type(TypeVariant::Record(rty)) => rty.is_empty_record(),
+            Self::Type {
+                ty: TypeVariant::Record(rty),
+                ..
+            } => rty.is_empty_record(),
             _ => false,
         }
     }
+
+    /// Get the source location of this [`Type`]
+    pub fn loc(&self) -> Option<&Loc> {
+        match self {
+            Self::Type { loc, .. } => loc.as_ref(),
+            Self::CommonTypeRef { loc, .. } => loc.as_ref(),
+        }
+    }
 }
 
 impl Type<RawName> {
@@ -1051,18 +1118,26 @@ impl Type<RawName> {
         ns: Option<&InternalName>,
     ) -> Type<ConditionalName> {
         match self {
-            Self::Type(tv) => Type::Type(tv.conditionally_qualify_type_references(ns)),
-            Self::CommonTypeRef { type_name } => Type::CommonTypeRef {
+            Self::Type { ty, loc } => Type::Type {
+                ty: ty.conditionally_qualify_type_references(ns),
+                loc,
+            },
+            Self::CommonTypeRef { type_name, loc } => Type::CommonTypeRef {
                 type_name: type_name.conditionally_qualify_with(ns, ReferenceType::Common),
+                loc,
             },
         }
     }
 
     fn into_n<N: From<RawName>>(self) -> Type<N> {
         match self {
-            Self::Type(tv) => Type::Type(tv.into_n()),
-            Self::CommonTypeRef { type_name } => Type::CommonTypeRef {
+            Self::Type { ty, loc } => Type::Type {
+                ty: ty.into_n(),
+                loc,
+            },
+            Self::CommonTypeRef { type_name, loc } => Type::CommonTypeRef {
                 type_name: type_name.into(),
+                loc,
             },
         }
     }
@@ -1079,9 +1154,13 @@ impl Type<ConditionalName> {
         all_defs: &AllDefs,
     ) -> std::result::Result<Type<InternalName>, TypeNotDefinedError> {
         match self {
-            Self::Type(tv) => Ok(Type::Type(tv.fully_qualify_type_references(all_defs)?)),
-            Self::CommonTypeRef { type_name } => Ok(Type::CommonTypeRef {
+            Self::Type { ty, loc } => Ok(Type::Type {
+                ty: ty.fully_qualify_type_references(all_defs)?,
+                loc,
+            }),
+            Self::CommonTypeRef { type_name, loc } => Ok(Type::CommonTypeRef {
                 type_name: type_name.resolve(all_defs)?,
+                loc,
             }),
         }
     }
@@ -1270,15 +1349,24 @@ impl<'de, N: Deserialize<'de> + From<RawName>> TypeVisitor<N> {
                 match s.as_str() {
                     "String" => {
                         error_if_any_fields()?;
-                        Ok(Type::Type(TypeVariant::String))
+                        Ok(Type::Type {
+                            ty: TypeVariant::String,
+                            loc: None,
+                        })
                     }
                     "Long" => {
                         error_if_any_fields()?;
-                        Ok(Type::Type(TypeVariant::Long))
+                        Ok(Type::Type {
+                            ty: TypeVariant::Long,
+                            loc: None,
+                        })
                     }
                     "Boolean" => {
                         error_if_any_fields()?;
-                        Ok(Type::Type(TypeVariant::Boolean))
+                        Ok(Type::Type {
+                            ty: TypeVariant::Boolean,
+                            loc: None,
+                        })
                     }
                     "Set" => {
                         error_if_fields(
@@ -1287,9 +1375,12 @@ impl<'de, N: Deserialize<'de> + From<RawName>> TypeVisitor<N> {
                         )?;
 
                         match element {
-                            Some(element) => Ok(Type::Type(TypeVariant::Set {
-                                element: Box::new(element),
-                            })),
+                            Some(element) => Ok(Type::Type {
+                                ty: TypeVariant::Set {
+                                    element: Box::new(element),
+                                },
+                                loc: None,
+                            }),
                             None => Err(serde::de::Error::missing_field(Element.as_str())),
                         }
                     }
@@ -1305,33 +1396,36 @@ impl<'de, N: Deserialize<'de> + From<RawName>> TypeVisitor<N> {
                         if let Some(attributes) = attributes {
                             let additional_attributes =
                                 additional_attributes.unwrap_or_else(partial_schema_default);
-                            Ok(Type::Type(TypeVariant::Record(RecordType {
-                                attributes: attributes
-                                    .0
-                                    .into_iter()
-                                    .map(
-                                        |(
-                                            k,
-                                            TypeOfAttribute {
-                                                ty,
-                                                required,
-                                                annotations,
-                                            },
-                                        )| {
-                                            (
+                            Ok(Type::Type {
+                                ty: TypeVariant::Record(RecordType {
+                                    attributes: attributes
+                                        .0
+                                        .into_iter()
+                                        .map(
+                                            |(
                                                 k,
                                                 TypeOfAttribute {
-                                                    ty: ty.into_n(),
-
+                                                    ty,
                                                     required,
                                                     annotations,
                                                 },
-                                            )
-                                        },
-                                    )
-                                    .collect(),
-                                additional_attributes,
-                            })))
+                                            )| {
+                                                (
+                                                    k,
+                                                    TypeOfAttribute {
+                                                        ty: ty.into_n(),
+
+                                                        required,
+                                                        annotations,
+                                                    },
+                                                )
+                                            },
+                                        )
+                                        .collect(),
+                                    additional_attributes,
+                                }),
+                                loc: None,
+                            })
                         } else {
                             Err(serde::de::Error::missing_field(Attributes.as_str()))
                         }
@@ -1342,15 +1436,18 @@ impl<'de, N: Deserialize<'de> + From<RawName>> TypeVisitor<N> {
                             &[type_field_name!(Name)],
                         )?;
                         match name {
-                            Some(name) => Ok(Type::Type(TypeVariant::Entity {
-                                name: RawName::from_normalized_str(&name)
-                                    .map_err(|err| {
-                                        serde::de::Error::custom(format!(
-                                            "invalid entity type `{name}`: {err}"
-                                        ))
-                                    })?
-                                    .into(),
-                            })),
+                            Some(name) => Ok(Type::Type {
+                                ty: TypeVariant::Entity {
+                                    name: RawName::from_normalized_str(&name)
+                                        .map_err(|err| {
+                                            serde::de::Error::custom(format!(
+                                                "invalid entity type `{name}`: {err}"
+                                            ))
+                                        })?
+                                        .into(),
+                                },
+                                loc: None,
+                            }),
                             None => Err(serde::de::Error::missing_field(Name.as_str())),
                         }
                     }
@@ -1360,15 +1457,18 @@ impl<'de, N: Deserialize<'de> + From<RawName>> TypeVisitor<N> {
                             &[type_field_name!(Name)],
                         )?;
                         match name {
-                            Some(name) => Ok(Type::Type(TypeVariant::EntityOrCommon {
-                                type_name: RawName::from_normalized_str(&name)
-                                    .map_err(|err| {
-                                        serde::de::Error::custom(format!(
-                                            "invalid entity or common type `{name}`: {err}"
-                                        ))
-                                    })?
-                                    .into(),
-                            })),
+                            Some(name) => Ok(Type::Type {
+                                ty: TypeVariant::EntityOrCommon {
+                                    type_name: RawName::from_normalized_str(&name)
+                                        .map_err(|err| {
+                                            serde::de::Error::custom(format!(
+                                                "invalid entity or common type `{name}`: {err}"
+                                            ))
+                                        })?
+                                        .into(),
+                                },
+                                loc: None,
+                            }),
                             None => Err(serde::de::Error::missing_field(Name.as_str())),
                         }
                     }
@@ -1379,13 +1479,18 @@ impl<'de, N: Deserialize<'de> + From<RawName>> TypeVisitor<N> {
                         )?;
 
                         match name {
-                            Some(name) => Ok(Type::Type(TypeVariant::Extension {
-                                name: UnreservedId::from_normalized_str(&name).map_err(|err| {
-                                    serde::de::Error::custom(format!(
-                                        "invalid extension type `{name}`: {err}"
-                                    ))
-                                })?,
-                            })),
+                            Some(name) => Ok(Type::Type {
+                                ty: TypeVariant::Extension {
+                                    name: UnreservedId::from_normalized_str(&name).map_err(
+                                        |err| {
+                                            serde::de::Error::custom(format!(
+                                                "invalid extension type `{name}`: {err}"
+                                            ))
+                                        },
+                                    )?,
+                                },
+                                loc: None,
+                            }),
                             None => Err(serde::de::Error::missing_field(Name.as_str())),
                         }
                     }
@@ -1399,6 +1504,7 @@ impl<'de, N: Deserialize<'de> + From<RawName>> TypeVisitor<N> {
                                     ))
                                 },
                             )?),
+                            loc: None,
                         })
                     }
                 }
@@ -1409,8 +1515,8 @@ impl<'de, N: Deserialize<'de> + From<RawName>> TypeVisitor<N> {
 }
 
 impl<N> From<TypeVariant<N>> for Type<N> {
-    fn from(variant: TypeVariant<N>) -> Self {
-        Self::Type(variant)
+    fn from(ty: TypeVariant<N>) -> Self {
+        Self::Type { ty, loc: None }
     }
 }
 
@@ -1419,7 +1525,8 @@ impl<N> From<TypeVariant<N>> for Type<N> {
 /// The parameter `N` is the type of entity type names and common type names in
 /// this [`RecordType`], including recursively.
 /// See notes on [`Fragment`].
-#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
+#[derive(Educe, Debug, Clone, Serialize, Deserialize)]
+#[educe(PartialEq, Eq, PartialOrd, Ord)]
 #[serde(bound(deserialize = "N: Deserialize<'de> + From<RawName>"))]
 #[serde(rename_all = "camelCase")]
 #[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
@@ -1495,7 +1602,8 @@ impl RecordType<ConditionalName> {
 /// The parameter `N` is the type of entity type names and common type names in
 /// this [`TypeVariant`], including recursively.
 /// See notes on [`Fragment`].
-#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
+#[derive(Educe, Debug, Clone, Serialize, Deserialize)]
+#[educe(PartialEq(bound(N: PartialEq)), Eq, PartialOrd, Ord)]
 #[serde(tag = "type")]
 #[serde(bound(deserialize = "N: Deserialize<'de> + From<RawName>"))]
 #[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
@@ -1699,46 +1807,49 @@ impl<'a> arbitrary::Arbitrary<'a> for Type<RawName> {
     fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Type<RawName>> {
         use std::collections::BTreeSet;
 
-        Ok(Type::Type(match u.int_in_range::<u8>(1..=8)? {
-            1 => TypeVariant::String,
-            2 => TypeVariant::Long,
-            3 => TypeVariant::Boolean,
-            4 => TypeVariant::Set {
-                element: Box::new(u.arbitrary()?),
-            },
-            5 => {
-                let attributes = {
-                    let attr_names: BTreeSet<String> = u.arbitrary()?;
-                    attr_names
-                        .into_iter()
-                        .map(|attr_name| {
-                            Ok((
-                                attr_name.into(),
-                                u.arbitrary::<TypeOfAttribute<RawName>>()?.into(),
-                            ))
-                        })
-                        .collect::<arbitrary::Result<_>>()?
-                };
-                TypeVariant::Record(RecordType {
-                    attributes,
-                    additional_attributes: u.arbitrary()?,
-                })
-            }
-            6 => TypeVariant::Entity {
-                name: u.arbitrary()?,
-            },
-            7 => TypeVariant::Extension {
-                // PANIC SAFETY: `ipaddr` is a valid `UnreservedId`
-                #[allow(clippy::unwrap_used)]
-                name: "ipaddr".parse().unwrap(),
-            },
-            8 => TypeVariant::Extension {
-                // PANIC SAFETY: `decimal` is a valid `UnreservedId`
-                #[allow(clippy::unwrap_used)]
-                name: "decimal".parse().unwrap(),
+        Ok(Type::Type {
+            ty: match u.int_in_range::<u8>(1..=8)? {
+                1 => TypeVariant::String,
+                2 => TypeVariant::Long,
+                3 => TypeVariant::Boolean,
+                4 => TypeVariant::Set {
+                    element: Box::new(u.arbitrary()?),
+                },
+                5 => {
+                    let attributes = {
+                        let attr_names: BTreeSet<String> = u.arbitrary()?;
+                        attr_names
+                            .into_iter()
+                            .map(|attr_name| {
+                                Ok((
+                                    attr_name.into(),
+                                    u.arbitrary::<TypeOfAttribute<RawName>>()?.into(),
+                                ))
+                            })
+                            .collect::<arbitrary::Result<_>>()?
+                    };
+                    TypeVariant::Record(RecordType {
+                        attributes,
+                        additional_attributes: u.arbitrary()?,
+                    })
+                }
+                6 => TypeVariant::Entity {
+                    name: u.arbitrary()?,
+                },
+                7 => TypeVariant::Extension {
+                    // PANIC SAFETY: `ipaddr` is a valid `UnreservedId`
+                    #[allow(clippy::unwrap_used)]
+                    name: "ipaddr".parse().unwrap(),
+                },
+                8 => TypeVariant::Extension {
+                    // PANIC SAFETY: `decimal` is a valid `UnreservedId`
+                    #[allow(clippy::unwrap_used)]
+                    name: "decimal".parse().unwrap(),
+                },
+                n => panic!("bad index: {n}"),
             },
-            n => panic!("bad index: {n}"),
-        }))
+            loc: None,
+        })
     }
     fn size_hint(_depth: usize) -> (usize, Option<usize>) {
         (1, None) // Unfortunately, we probably can't be more precise than this
@@ -1763,7 +1874,8 @@ impl<'a> arbitrary::Arbitrary<'a> for Type<RawName> {
 /// (`<https://github.com/serde-rs/serde/issues/1600>`). This should be ok because
 /// unknown fields for [`TypeOfAttribute`] should be passed to [`Type`] where
 /// they will be denied (`<https://github.com/serde-rs/serde/issues/1600>`).
-#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Eq, PartialOrd, Ord)]
+#[derive(Educe, Debug, Clone, Serialize, Deserialize)]
+#[educe(PartialEq, Eq, PartialOrd, Ord)]
 #[serde(bound(deserialize = "N: Deserialize<'de> + From<RawName>"))]
 pub struct TypeOfAttribute<N> {
     /// Underlying type of the attribute
@@ -1879,10 +1991,13 @@ mod test {
         assert_eq!(et.member_of_types, vec!["UserGroup".parse().unwrap()]);
         assert_eq!(
             et.shape,
-            AttributesOrContext(Type::Type(TypeVariant::Record(RecordType {
-                attributes: BTreeMap::new(),
-                additional_attributes: false
-            }))),
+            AttributesOrContext(Type::Type {
+                ty: TypeVariant::Record(RecordType {
+                    attributes: BTreeMap::new(),
+                    additional_attributes: false
+                }),
+                loc: None
+            }),
         );
     }
 
@@ -1895,10 +2010,13 @@ mod test {
         assert_eq!(et.member_of_types.len(), 0);
         assert_eq!(
             et.shape,
-            AttributesOrContext(Type::Type(TypeVariant::Record(RecordType {
-                attributes: BTreeMap::new(),
-                additional_attributes: false
-            }))),
+            AttributesOrContext(Type::Type {
+                ty: TypeVariant::Record(RecordType {
+                    attributes: BTreeMap::new(),
+                    additional_attributes: false
+                }),
+                loc: None
+            }),
         );
     }
 
@@ -2696,12 +2814,12 @@ mod entity_tags {
         let json = example_json_schema();
         assert_matches!(Fragment::from_json_value(json), Ok(frag) => {
             let user = &frag.0.get(&None).unwrap().entity_types.get(&"User".parse().unwrap()).unwrap();
-            assert_matches!(&user.tags, Some(Type::Type(TypeVariant::Set { element })) => {
-                assert_matches!(&**element, Type::Type(TypeVariant::String)); // TODO: why is this `TypeVariant::String` in this case but `EntityOrCommon { "String" }` in all the other cases in this test? Do we accept common types as the element type for sets?
+            assert_matches!(&user.tags, Some(Type::Type { ty: TypeVariant::Set { element }, loc: None }) => {
+                assert_matches!(&**element, Type::Type { ty: TypeVariant::String, loc: None }); // TODO: why is this `TypeVariant::String` in this case but `EntityOrCommon { "String" }` in all the other cases in this test? Do we accept common types as the element type for sets?
             });
             let doc = &frag.0.get(&None).unwrap().entity_types.get(&"Document".parse().unwrap()).unwrap();
-            assert_matches!(&doc.tags, Some(Type::Type(TypeVariant::Set { element })) => {
-                assert_matches!(&**element, Type::Type(TypeVariant::String)); // TODO: why is this `TypeVariant::String` in this case but `EntityOrCommon { "String" }` in all the other cases in this test? Do we accept common types as the element type for sets?
+            assert_matches!(&doc.tags, Some(Type::Type { ty: TypeVariant::Set { element }, loc: None }) => {
+                assert_matches!(&**element, Type::Type { ty: TypeVariant::String, loc: None }); // TODO: why is this `TypeVariant::String` in this case but `EntityOrCommon { "String" }` in all the other cases in this test? Do we accept common types as the element type for sets?
             });
         })
     }
@@ -2730,7 +2848,7 @@ mod entity_tags {
         }});
         assert_matches!(Fragment::from_json_value(json), Ok(frag) => {
             let user = &frag.0.get(&None).unwrap().entity_types.get(&"User".parse().unwrap()).unwrap();
-            assert_matches!(&user.tags, Some(Type::CommonTypeRef { type_name }) => {
+            assert_matches!(&user.tags, Some(Type::CommonTypeRef { type_name, loc: None }) => {
                 assert_eq!(&format!("{type_name}"), "T");
             });
         })
@@ -2757,7 +2875,7 @@ mod entity_tags {
         }});
         assert_matches!(Fragment::from_json_value(json), Ok(frag) => {
             let user = &frag.0.get(&None).unwrap().entity_types.get(&"User".parse().unwrap()).unwrap();
-            assert_matches!(&user.tags, Some(Type::Type(TypeVariant::Entity{ name })) => {
+            assert_matches!(&user.tags, Some(Type::Type { ty: TypeVariant::Entity { name }, loc: None }) => {
                 assert_eq!(&format!("{name}"), "User");
             });
         })
@@ -2807,15 +2925,7 @@ mod test_json_roundtrip {
 
     #[test]
     fn empty_namespace() {
-        let fragment = Fragment(BTreeMap::from([(
-            None,
-            NamespaceDefinition {
-                common_types: BTreeMap::new(),
-                entity_types: BTreeMap::new(),
-                actions: BTreeMap::new(),
-                annotations: Annotations::new(),
-            },
-        )]));
+        let fragment = Fragment(BTreeMap::from([(None, NamespaceDefinition::new([], []))]));
         roundtrip(fragment);
     }
 
@@ -2823,12 +2933,7 @@ mod test_json_roundtrip {
     fn nonempty_namespace() {
         let fragment = Fragment(BTreeMap::from([(
             Some("a".parse().unwrap()),
-            NamespaceDefinition {
-                common_types: BTreeMap::new(),
-                entity_types: BTreeMap::new(),
-                actions: BTreeMap::new(),
-                annotations: Annotations::new(),
-            },
+            NamespaceDefinition::new([], []),
         )]));
         roundtrip(fragment);
     }
@@ -2837,42 +2942,44 @@ mod test_json_roundtrip {
     fn nonempty_entity_types() {
         let fragment = Fragment(BTreeMap::from([(
             None,
-            NamespaceDefinition {
-                common_types: BTreeMap::new(),
-                entity_types: BTreeMap::from([(
+            NamespaceDefinition::new(
+                [(
                     "a".parse().unwrap(),
                     EntityType {
                         member_of_types: vec!["a".parse().unwrap()],
-                        shape: AttributesOrContext(Type::Type(TypeVariant::Record(RecordType {
-                            attributes: BTreeMap::new(),
-                            additional_attributes: false,
-                        }))),
+                        shape: AttributesOrContext(Type::Type {
+                            ty: TypeVariant::Record(RecordType {
+                                attributes: BTreeMap::new(),
+                                additional_attributes: false,
+                            }),
+                            loc: None,
+                        }),
                         tags: None,
                         annotations: Annotations::new(),
                         loc: None,
                     },
-                )]),
-                actions: BTreeMap::from([(
+                )],
+                [(
                     "action".into(),
                     ActionType {
                         attributes: None,
                         applies_to: Some(ApplySpec {
                             resource_types: vec!["a".parse().unwrap()],
                             principal_types: vec!["a".parse().unwrap()],
-                            context: AttributesOrContext(Type::Type(TypeVariant::Record(
-                                RecordType {
+                            context: AttributesOrContext(Type::Type {
+                                ty: TypeVariant::Record(RecordType {
                                     attributes: BTreeMap::new(),
                                     additional_attributes: false,
-                                },
-                            ))),
+                                }),
+                                loc: None,
+                            }),
                         }),
                         member_of: None,
                         annotations: Annotations::new(),
                         loc: None,
                     },
-                )]),
-                annotations: Annotations::new(),
-            },
+                )],
+            ),
         )]));
         roundtrip(fragment);
     }
@@ -2882,53 +2989,51 @@ mod test_json_roundtrip {
         let fragment = Fragment(BTreeMap::from([
             (
                 Some("foo".parse().unwrap()),
-                NamespaceDefinition {
-                    common_types: BTreeMap::new(),
-                    entity_types: BTreeMap::from([(
+                NamespaceDefinition::new(
+                    [(
                         "a".parse().unwrap(),
                         EntityType {
                             member_of_types: vec!["a".parse().unwrap()],
-                            shape: AttributesOrContext(Type::Type(TypeVariant::Record(
-                                RecordType {
+                            shape: AttributesOrContext(Type::Type {
+                                ty: TypeVariant::Record(RecordType {
                                     attributes: BTreeMap::new(),
                                     additional_attributes: false,
-                                },
-                            ))),
+                                }),
+                                loc: None,
+                            }),
                             tags: None,
                             annotations: Annotations::new(),
                             loc: None,
                         },
-                    )]),
-                    actions: BTreeMap::new(),
-                    annotations: Annotations::new(),
-                },
+                    )],
+                    [],
+                ),
             ),
             (
                 None,
-                NamespaceDefinition {
-                    common_types: BTreeMap::new(),
-                    entity_types: BTreeMap::new(),
-                    actions: BTreeMap::from([(
+                NamespaceDefinition::new(
+                    [],
+                    [(
                         "action".into(),
                         ActionType {
                             attributes: None,
                             applies_to: Some(ApplySpec {
                                 resource_types: vec!["foo::a".parse().unwrap()],
                                 principal_types: vec!["foo::a".parse().unwrap()],
-                                context: AttributesOrContext(Type::Type(TypeVariant::Record(
-                                    RecordType {
+                                context: AttributesOrContext(Type::Type {
+                                    ty: TypeVariant::Record(RecordType {
                                         attributes: BTreeMap::new(),
                                         additional_attributes: false,
-                                    },
-                                ))),
+                                    }),
+                                    loc: None,
+                                }),
                             }),
                             member_of: None,
                             annotations: Annotations::new(),
                             loc: None,
                         },
-                    )]),
-                    annotations: Annotations::new(),
-                },
+                    )],
+                ),
             ),
         ]));
         roundtrip(fragment);
diff --git a/cedar-policy-validator/src/schema.rs b/cedar-policy-validator/src/schema.rs
index c668b04d..36b21d38 100644
--- a/cedar-policy-validator/src/schema.rs
+++ b/cedar-policy-validator/src/schema.rs
@@ -24,6 +24,7 @@ use cedar_policy_core::{
     ast::{Entity, EntityType, EntityUID, InternalName, Name, UnreservedId},
     entities::{err::EntitiesError, Entities, TCComputation},
     extensions::Extensions,
+    parser::Loc,
     transitive_closure::compute_tc,
 };
 use itertools::Itertools;
@@ -421,6 +422,7 @@ impl ValidatorSchema {
                 fragments.push(single_alias_in_empty_namespace(
                     tyname.basename().clone(),
                     tyname.as_ref().qualify_with(Some(&InternalName::__cedar())),
+                    None, // there is no source loc associated with the builtin definitions of primitive and extension types
                 ));
                 all_defs.mark_as_defined_as_common_type(tyname.into());
             }
@@ -983,7 +985,10 @@ fn cedar_fragment(
         let ext_type = ext_type.basename().clone();
         common_types.insert(
             ext_type.clone(),
-            json_schema::Type::Type(json_schema::TypeVariant::Extension { name: ext_type }),
+            json_schema::Type::Type {
+                ty: json_schema::TypeVariant::Extension { name: ext_type },
+                loc: None,
+            },
         );
     }
 
@@ -1006,14 +1011,18 @@ fn cedar_fragment(
 fn single_alias_in_empty_namespace(
     id: UnreservedId,
     def: InternalName,
+    loc: Option<Loc>,
 ) -> ValidatorSchemaFragment<ConditionalName, ConditionalName> {
     ValidatorSchemaFragment(vec![ValidatorNamespaceDef::from_common_type_def(
         None,
         (
             id,
-            json_schema::Type::Type(json_schema::TypeVariant::EntityOrCommon {
-                type_name: ConditionalName::unconditional(def, ReferenceType::CommonOrEntity),
-            }),
+            json_schema::Type::Type {
+                ty: json_schema::TypeVariant::EntityOrCommon {
+                    type_name: ConditionalName::unconditional(def, ReferenceType::CommonOrEntity),
+                },
+                loc,
+            },
         ),
     )])
 }
@@ -1026,15 +1035,24 @@ fn primitive_types<N>() -> impl Iterator<Item = (UnreservedId, json_schema::Type
     [
         (
             UnreservedId::from_str("Bool").unwrap(),
-            json_schema::Type::Type(json_schema::TypeVariant::Boolean),
+            json_schema::Type::Type {
+                ty: json_schema::TypeVariant::Boolean,
+                loc: None,
+            },
         ),
         (
             UnreservedId::from_str("Long").unwrap(),
-            json_schema::Type::Type(json_schema::TypeVariant::Long),
+            json_schema::Type::Type {
+                ty: json_schema::TypeVariant::Long,
+                loc: None,
+            },
         ),
         (
             UnreservedId::from_str("String").unwrap(),
-            json_schema::Type::Type(json_schema::TypeVariant::String),
+            json_schema::Type::Type {
+                ty: json_schema::TypeVariant::String,
+                loc: None,
+            },
         ),
     ]
     .into_iter()
@@ -1322,36 +1340,47 @@ impl<'a> CommonTypeResolver<'a> {
         }
     }
 
-    // Substitute common type references in `ty` according to `resolve_table`
+    // Substitute common type references in `ty` according to `resolve_table`.
+    // Resolved types will still have the source loc of `ty`, unless `ty` is
+    // exactly a common type reference, in which case they will have the source
+    // loc of the definition of that reference.
     fn resolve_type(
         resolve_table: &HashMap<&InternalName, json_schema::Type<InternalName>>,
         ty: json_schema::Type<InternalName>,
     ) -> Result<json_schema::Type<InternalName>> {
         match ty {
-            json_schema::Type::CommonTypeRef { type_name } => resolve_table
+            json_schema::Type::CommonTypeRef { type_name, .. } => resolve_table
                 .get(&type_name)
                 .ok_or_else(|| CommonTypeInvariantViolationError { name: type_name }.into())
                 .cloned(),
-            json_schema::Type::Type(json_schema::TypeVariant::EntityOrCommon { type_name }) => {
-                match resolve_table.get(&type_name) {
-                    Some(def) => Ok(def.clone()),
-                    None => Ok(json_schema::Type::Type(json_schema::TypeVariant::Entity {
-                        name: type_name,
-                    })),
-                }
-            }
-            json_schema::Type::Type(json_schema::TypeVariant::Set { element }) => {
-                Ok(json_schema::Type::Type(json_schema::TypeVariant::Set {
+            json_schema::Type::Type {
+                ty: json_schema::TypeVariant::EntityOrCommon { type_name },
+                loc,
+            } => match resolve_table.get(&type_name) {
+                Some(def) => Ok(def.clone()),
+                None => Ok(json_schema::Type::Type {
+                    ty: json_schema::TypeVariant::Entity { name: type_name },
+                    loc,
+                }),
+            },
+            json_schema::Type::Type {
+                ty: json_schema::TypeVariant::Set { element },
+                loc,
+            } => Ok(json_schema::Type::Type {
+                ty: json_schema::TypeVariant::Set {
                     element: Box::new(Self::resolve_type(resolve_table, *element)?),
-                }))
-            }
-            json_schema::Type::Type(json_schema::TypeVariant::Record(
-                json_schema::RecordType {
-                    attributes,
-                    additional_attributes,
                 },
-            )) => Ok(json_schema::Type::Type(json_schema::TypeVariant::Record(
-                json_schema::RecordType {
+                loc,
+            }),
+            json_schema::Type::Type {
+                ty:
+                    json_schema::TypeVariant::Record(json_schema::RecordType {
+                        attributes,
+                        additional_attributes,
+                    }),
+                loc,
+            } => Ok(json_schema::Type::Type {
+                ty: json_schema::TypeVariant::Record(json_schema::RecordType {
                     attributes: BTreeMap::from_iter(
                         attributes
                             .into_iter()
@@ -1368,8 +1397,9 @@ impl<'a> CommonTypeResolver<'a> {
                             .collect::<Result<Vec<(_, _)>>>()?,
                     ),
                     additional_attributes,
-                },
-            ))),
+                }),
+                loc,
+            }),
             _ => Ok(ty),
         }
     }
@@ -2000,9 +2030,12 @@ pub(crate) mod test {
         let schema_ty: json_schema::Type<RawName> = serde_json::from_value(src).unwrap();
         assert_eq!(
             schema_ty,
-            json_schema::Type::Type(json_schema::TypeVariant::Entity {
-                name: "Foo".parse().unwrap()
-            })
+            json_schema::Type::Type {
+                ty: json_schema::TypeVariant::Entity {
+                    name: "Foo".parse().unwrap()
+                },
+                loc: None
+            },
         );
         let schema_ty = schema_ty.conditionally_qualify_type_references(Some(
             &InternalName::parse_unqualified_name("NS").unwrap(),
@@ -2026,9 +2059,12 @@ pub(crate) mod test {
         let schema_ty: json_schema::Type<RawName> = serde_json::from_value(src).unwrap();
         assert_eq!(
             schema_ty,
-            json_schema::Type::Type(json_schema::TypeVariant::Entity {
-                name: "NS::Foo".parse().unwrap()
-            })
+            json_schema::Type::Type {
+                ty: json_schema::TypeVariant::Entity {
+                    name: "NS::Foo".parse().unwrap()
+                },
+                loc: None
+            },
         );
         let schema_ty = schema_ty.conditionally_qualify_type_references(Some(
             &InternalName::parse_unqualified_name("NS").unwrap(),
@@ -2061,10 +2097,13 @@ pub(crate) mod test {
         let schema_ty: json_schema::Type<RawName> = serde_json::from_value(src).unwrap();
         assert_eq!(
             schema_ty,
-            json_schema::Type::Type(json_schema::TypeVariant::Record(json_schema::RecordType {
-                attributes: BTreeMap::new(),
-                additional_attributes: false,
-            })),
+            json_schema::Type::Type {
+                ty: json_schema::TypeVariant::Record(json_schema::RecordType {
+                    attributes: BTreeMap::new(),
+                    additional_attributes: false,
+                }),
+                loc: None
+            },
         );
         let schema_ty = schema_ty.conditionally_qualify_type_references(None);
         let all_defs = AllDefs::from_entity_defs([InternalName::from_str("Foo").unwrap()]);
diff --git a/cedar-policy-validator/src/schema/namespace_def.rs b/cedar-policy-validator/src/schema/namespace_def.rs
index 5c98bf68..64eac82f 100644
--- a/cedar-policy-validator/src/schema/namespace_def.rs
+++ b/cedar-policy-validator/src/schema/namespace_def.rs
@@ -941,25 +941,34 @@ pub(crate) fn try_jsonschema_type_into_validator_type(
     extensions: &Extensions<'_>,
 ) -> crate::err::Result<WithUnresolvedCommonTypeRefs<Type>> {
     match schema_ty {
-        json_schema::Type::Type(json_schema::TypeVariant::String) => {
-            Ok(Type::primitive_string().into())
-        }
-        json_schema::Type::Type(json_schema::TypeVariant::Long) => {
-            Ok(Type::primitive_long().into())
-        }
-        json_schema::Type::Type(json_schema::TypeVariant::Boolean) => {
-            Ok(Type::primitive_boolean().into())
-        }
-        json_schema::Type::Type(json_schema::TypeVariant::Set { element }) => {
-            Ok(try_jsonschema_type_into_validator_type(*element, extensions)?.map(Type::set))
-        }
-        json_schema::Type::Type(json_schema::TypeVariant::Record(rty)) => {
-            try_record_type_into_validator_type(rty, extensions)
-        }
-        json_schema::Type::Type(json_schema::TypeVariant::Entity { name }) => {
-            Ok(Type::named_entity_reference(internal_name_to_entity_type(name)?).into())
-        }
-        json_schema::Type::Type(json_schema::TypeVariant::Extension { name }) => {
+        json_schema::Type::Type {
+            ty: json_schema::TypeVariant::String,
+            ..
+        } => Ok(Type::primitive_string().into()),
+        json_schema::Type::Type {
+            ty: json_schema::TypeVariant::Long,
+            ..
+        } => Ok(Type::primitive_long().into()),
+        json_schema::Type::Type {
+            ty: json_schema::TypeVariant::Boolean,
+            ..
+        } => Ok(Type::primitive_boolean().into()),
+        json_schema::Type::Type {
+            ty: json_schema::TypeVariant::Set { element },
+            ..
+        } => Ok(try_jsonschema_type_into_validator_type(*element, extensions)?.map(Type::set)),
+        json_schema::Type::Type {
+            ty: json_schema::TypeVariant::Record(rty),
+            ..
+        } => try_record_type_into_validator_type(rty, extensions),
+        json_schema::Type::Type {
+            ty: json_schema::TypeVariant::Entity { name },
+            ..
+        } => Ok(Type::named_entity_reference(internal_name_to_entity_type(name)?).into()),
+        json_schema::Type::Type {
+            ty: json_schema::TypeVariant::Extension { name },
+            ..
+        } => {
             let extension_type_name = Name::unqualified_name(name);
             if extensions.ext_types().contains(&extension_type_name) {
                 Ok(Type::extension(extension_type_name).into())
@@ -979,7 +988,7 @@ pub(crate) fn try_jsonschema_type_into_validator_type(
                 ))
             }
         }
-        json_schema::Type::CommonTypeRef { type_name } => {
+        json_schema::Type::CommonTypeRef { type_name, .. } => {
             Ok(WithUnresolvedCommonTypeRefs::new(move |common_type_defs| {
                 common_type_defs
                     .get(&type_name)
@@ -995,7 +1004,10 @@ pub(crate) fn try_jsonschema_type_into_validator_type(
                     .ok_or_else(|| CommonTypeInvariantViolationError { name: type_name }.into())
             }))
         }
-        json_schema::Type::Type(json_schema::TypeVariant::EntityOrCommon { type_name }) => {
+        json_schema::Type::Type {
+            ty: json_schema::TypeVariant::EntityOrCommon { type_name },
+            ..
+        } => {
             Ok(WithUnresolvedCommonTypeRefs::new(move |common_type_defs| {
                 // First check if it's a common type, because in the edge case where
                 // the name is both a valid common type name and a valid entity type
diff --git a/cedar-policy-validator/src/schema/raw_name.rs b/cedar-policy-validator/src/schema/raw_name.rs
index 5453219b..8b81b4d5 100644
--- a/cedar-policy-validator/src/schema/raw_name.rs
+++ b/cedar-policy-validator/src/schema/raw_name.rs
@@ -17,6 +17,7 @@
 use crate::schema::AllDefs;
 use crate::schema_errors::TypeNotDefinedError;
 use cedar_policy_core::ast::{Id, InternalName, Name, UnreservedId};
+use cedar_policy_core::parser::Loc;
 use itertools::Itertools;
 use nonempty::{nonempty, NonEmpty};
 use serde::{Deserialize, Serialize};
@@ -83,6 +84,11 @@ impl RawName {
         self.0.is_unqualified()
     }
 
+    /// Get the source location of this `RawName`
+    pub fn loc(&self) -> Option<&Loc> {
+        self.0.loc()
+    }
+
     /// Convert this [`RawName`] to an [`InternalName`] by adding the given `ns`
     /// as its prefix, or by no-op if `ns` is `None`.
     ///


Test patch:
diff --git a/cedar-policy-validator/src/cedar_schema/test.rs b/cedar-policy-validator/src/cedar_schema/test.rs
index c345b128..1813d125 100644
--- a/cedar-policy-validator/src/cedar_schema/test.rs
+++ b/cedar-policy-validator/src/cedar_schema/test.rs
@@ -219,28 +219,19 @@ mod demo_tests {
             json_schema::Fragment::from_cedarschema_str(src, Extensions::all_available()).unwrap();
         let unqual = schema.0.get(&None).unwrap();
         let foo = unqual.actions.get("Foo").unwrap();
-        assert_matches!(&foo,
-                json_schema::ActionType {
-                    applies_to : Some(json_schema::ApplySpec {
-                        resource_types,
-                        principal_types,
-                        ..
-                    }),
-                    ..
-                } =>
-                {
-                    assert_matches!(principal_types.as_slice(), [a,b] => {
-                        assert_eq!(a, &"a".parse().unwrap());
-                        assert_eq!(b, &"b".parse().unwrap());
-                });
-                assert_matches!(resource_types.as_slice(), [c,d] =>  {
-                        assert_eq!(c, &"c".parse().unwrap());
-                        assert_eq!(d, &"d".parse().unwrap());
-
-                })
-            }
-            ,
-        );
+        assert_matches!(&foo, json_schema::ActionType {
+            applies_to: Some(json_schema::ApplySpec { resource_types, principal_types, .. }),
+            ..
+        } => {
+            assert_matches!(principal_types.as_slice(), [a,b] => {
+                assert_eq!(a, &"a".parse().unwrap());
+                assert_eq!(b, &"b".parse().unwrap());
+            });
+            assert_matches!(resource_types.as_slice(), [c,d] =>  {
+                assert_eq!(c, &"c".parse().unwrap());
+                assert_eq!(d, &"d".parse().unwrap());
+            });
+        });
     }
 
     #[test]
@@ -259,28 +250,19 @@ mod demo_tests {
             json_schema::Fragment::from_cedarschema_str(src, Extensions::all_available()).unwrap();
         let unqual = schema.0.get(&None).unwrap();
         let foo = unqual.actions.get("Foo").unwrap();
-        assert_matches!(foo,
-                json_schema::ActionType {
-                    applies_to : Some(json_schema::ApplySpec {
-                        resource_types,
-                        principal_types,
-                        ..
-                    }),
-                    ..
-                } =>
-                {
-                    assert_matches!(principal_types.as_slice(), [a,b] => {
-                        assert_eq!(a, &"a".parse().unwrap());
-                        assert_eq!(b, &"b".parse().unwrap());
-                });
-                assert_matches!(resource_types.as_slice(), [c,d] =>  {
-                        assert_eq!(c, &"c".parse().unwrap());
-                        assert_eq!(d, &"d".parse().unwrap());
-
-                })
-            }
-            ,
-        );
+        assert_matches!(foo, json_schema::ActionType {
+            applies_to: Some(json_schema::ApplySpec { resource_types, principal_types, .. }),
+            ..
+        } => {
+            assert_matches!(principal_types.as_slice(), [a,b] => {
+                assert_eq!(a, &"a".parse().unwrap());
+                assert_eq!(b, &"b".parse().unwrap());
+            });
+            assert_matches!(resource_types.as_slice(), [c,d] =>  {
+                assert_eq!(c, &"c".parse().unwrap());
+                assert_eq!(d, &"d".parse().unwrap());
+            });
+        });
     }
 
     #[test]
@@ -437,9 +419,8 @@ namespace Baz {action "Foo" appliesTo {
 
     #[test]
     fn print_actions() {
-        let namespace = json_schema::NamespaceDefinition {
-            common_types: BTreeMap::new(),
-            entity_types: BTreeMap::from([(
+        let namespace = json_schema::NamespaceDefinition::new(
+            [(
                 "a".parse().unwrap(),
                 json_schema::EntityType::<RawName> {
                     member_of_types: vec![],
@@ -448,8 +429,8 @@ namespace Baz {action "Foo" appliesTo {
                     annotations: Annotations::new(),
                     loc: None,
                 },
-            )]),
-            actions: BTreeMap::from([(
+            )],
+            [(
                 "j".to_smolstr(),
                 json_schema::ActionType::<RawName> {
                     attributes: None,
@@ -462,9 +443,8 @@ namespace Baz {action "Foo" appliesTo {
                     annotations: Annotations::new(),
                     loc: None,
                 },
-            )]),
-            annotations: Annotations::new(),
-        };
+            )],
+        );
         let fragment = json_schema::Fragment(BTreeMap::from([(None, namespace)]));
         let src = fragment.to_cedarschema().unwrap();
         assert!(src.contains(r#"action "j";"#), "schema was: `{src}`")
@@ -574,14 +554,14 @@ namespace Baz {action "Foo" appliesTo {
         assert!(repo.member_of_types.is_empty());
         let groups = ["readers", "writers", "triagers", "admins", "maintainers"];
         for group in groups {
-            assert_matches!(&repo.shape, json_schema::AttributesOrContext(json_schema::Type::Type(json_schema::TypeVariant::Record(json_schema::RecordType {
+            assert_matches!(&repo.shape, json_schema::AttributesOrContext(json_schema::Type::Type { ty: json_schema::TypeVariant::Record(json_schema::RecordType {
                 attributes,
                 additional_attributes: false,
-            }))) => {
+            }), loc: Some(_) }) => {
                 let expected =
-                    json_schema::Type::Type(json_schema::TypeVariant::EntityOrCommon {
+                    json_schema::Type::Type { ty: json_schema::TypeVariant::EntityOrCommon {
                         type_name: "UserGroup".parse().unwrap(),
-                    });
+                    }, loc: None};
                 let attribute = attributes.get(group).expect("No attribute `{group}`");
                 assert_has_type(attribute, expected);
             });
@@ -591,23 +571,23 @@ namespace Baz {action "Foo" appliesTo {
             .get(&"Issue".parse().unwrap())
             .expect("No `Issue`");
         assert!(issue.member_of_types.is_empty());
-        assert_matches!(&issue.shape, json_schema::AttributesOrContext(json_schema::Type::Type(json_schema::TypeVariant::Record(json_schema::RecordType {
+        assert_matches!(&issue.shape, json_schema::AttributesOrContext(json_schema::Type::Type { ty: json_schema::TypeVariant::Record(json_schema::RecordType {
             attributes,
             additional_attributes: false,
-        }))) => {
+        }), loc: Some(_) }) => {
             let attribute = attributes.get("repo").expect("No `repo`");
             assert_has_type(
                 attribute,
-                json_schema::Type::Type(json_schema::TypeVariant::EntityOrCommon {
+                json_schema::Type::Type { ty: json_schema::TypeVariant::EntityOrCommon {
                     type_name: "Repository".parse().unwrap(),
-                }),
+                }, loc: None },
             );
             let attribute = attributes.get("reporter").expect("No `repo`");
             assert_has_type(
                 attribute,
-                json_schema::Type::Type(json_schema::TypeVariant::EntityOrCommon {
+                json_schema::Type::Type { ty: json_schema::TypeVariant::EntityOrCommon {
                     type_name: "User".parse().unwrap(),
-                }),
+                }, loc: None },
             );
         });
         let org = github
@@ -617,13 +597,13 @@ namespace Baz {action "Foo" appliesTo {
         assert!(org.member_of_types.is_empty());
         let groups = ["members", "owners", "memberOfTypes"];
         for group in groups {
-            assert_matches!(&org.shape, json_schema::AttributesOrContext(json_schema::Type::Type(json_schema::TypeVariant::Record(json_schema::RecordType {
+            assert_matches!(&org.shape, json_schema::AttributesOrContext(json_schema::Type::Type { ty: json_schema::TypeVariant::Record(json_schema::RecordType {
                 attributes,
                 additional_attributes: false,
-            }))) => {
-                let expected = json_schema::Type::Type(json_schema::TypeVariant::EntityOrCommon {
+            }), loc: Some(_) }) => {
+                let expected = json_schema::Type::Type { ty: json_schema::TypeVariant::EntityOrCommon {
                     type_name: "UserGroup".parse().unwrap(),
-                });
+                }, loc: None };
                 let attribute = attributes.get(group).expect("No attribute `{group}`");
                 assert_has_type(attribute, expected);
             });
@@ -680,23 +660,23 @@ namespace Baz {action "Foo" appliesTo {
             .get(&"User".parse().unwrap())
             .expect("No `User`");
         assert_eq!(&user.member_of_types, &vec!["Group".parse().unwrap()]);
-        assert_matches!(&user.shape, json_schema::AttributesOrContext(json_schema::Type::Type(json_schema::TypeVariant::Record(json_schema::RecordType {
+        assert_matches!(&user.shape, json_schema::AttributesOrContext(json_schema::Type::Type { ty: json_schema::TypeVariant::Record(json_schema::RecordType {
             attributes,
             additional_attributes: false,
-        }))) => {
+        }), loc: Some(_) }) => {
             assert_has_type(
                 attributes.get("personalGroup").unwrap(),
-                json_schema::Type::Type(json_schema::TypeVariant::EntityOrCommon {
+                json_schema::Type::Type { ty: json_schema::TypeVariant::EntityOrCommon {
                     type_name: "Group".parse().unwrap(),
-                }),
+                }, loc: None }, // we do expect a `loc`, but `assert_has_type()` will ignore the mismatch in presence of `loc`. We have separate tests for the correctness of `loc`s coming from the Cedar schema syntax in a test module called `preserves_source_locations`.
             );
             assert_has_type(
                 attributes.get("blocked").unwrap(),
-                json_schema::Type::Type(json_schema::TypeVariant::Set {
-                    element: Box::new(json_schema::Type::Type(json_schema::TypeVariant::EntityOrCommon {
+                json_schema::Type::Type { ty: json_schema::TypeVariant::Set {
+                    element: Box::new(json_schema::Type::Type { ty: json_schema::TypeVariant::EntityOrCommon {
                         type_name: "User".parse().unwrap(),
-                    })),
-                }),
+                    }, loc: None }), // we do expect a `loc`, but `assert_has_type()` will ignore the mismatch in presence of `loc`. We have separate tests for the correctness of `loc`s coming from the Cedar schema syntax in a test module called `preserves_source_locations`.
+                }, loc: None },
             );
         });
         let group = doccloud
@@ -707,15 +687,15 @@ namespace Baz {action "Foo" appliesTo {
             &group.member_of_types,
             &vec!["DocumentShare".parse().unwrap()]
         );
-        assert_matches!(&group.shape, json_schema::AttributesOrContext(json_schema::Type::Type(json_schema::TypeVariant::Record(json_schema::RecordType {
+        assert_matches!(&group.shape, json_schema::AttributesOrContext(json_schema::Type::Type { ty: json_schema::TypeVariant::Record(json_schema::RecordType {
             attributes,
             additional_attributes: false,
-        }))) => {
+        }), loc: Some(_) }) => {
             assert_has_type(
                 attributes.get("owner").unwrap(),
-                json_schema::Type::Type(json_schema::TypeVariant::EntityOrCommon {
+                json_schema::Type::Type { ty: json_schema::TypeVariant::EntityOrCommon {
                     type_name: "User".parse().unwrap(),
-                }),
+                }, loc: None },
             );
         });
         let document = doccloud
@@ -723,45 +703,45 @@ namespace Baz {action "Foo" appliesTo {
             .get(&"Document".parse().unwrap())
             .expect("No `Group`");
         assert!(document.member_of_types.is_empty());
-        assert_matches!(&document.shape, json_schema::AttributesOrContext(json_schema::Type::Type(json_schema::TypeVariant::Record(json_schema::RecordType {
+        assert_matches!(&document.shape, json_schema::AttributesOrContext(json_schema::Type::Type { ty: json_schema::TypeVariant::Record(json_schema::RecordType {
             attributes,
             additional_attributes: false,
-        }))) => {
+        }), loc: Some(_) }) => {
             assert_has_type(
                 attributes.get("owner").unwrap(),
-                json_schema::Type::Type(json_schema::TypeVariant::EntityOrCommon {
+                json_schema::Type::Type { ty: json_schema::TypeVariant::EntityOrCommon {
                     type_name: "User".parse().unwrap(),
-                }),
+                }, loc: None },
             );
             assert_has_type(
                 attributes.get("isPrivate").unwrap(),
-                json_schema::Type::Type(json_schema::TypeVariant::EntityOrCommon {
+                json_schema::Type::Type { ty: json_schema::TypeVariant::EntityOrCommon {
                     type_name: "Bool".parse().unwrap(),
-                }),
+                }, loc: None },
             );
             assert_has_type(
                 attributes.get("publicAccess").unwrap(),
-                json_schema::Type::Type(json_schema::TypeVariant::EntityOrCommon {
+                json_schema::Type::Type { ty: json_schema::TypeVariant::EntityOrCommon {
                     type_name: "String".parse().unwrap(),
-                }),
+                }, loc: None },
             );
             assert_has_type(
                 attributes.get("viewACL").unwrap(),
-                json_schema::Type::Type(json_schema::TypeVariant::EntityOrCommon {
+                json_schema::Type::Type { ty: json_schema::TypeVariant::EntityOrCommon {
                     type_name: "DocumentShare".parse().unwrap(),
-                }),
+                }, loc: None },
             );
             assert_has_type(
                 attributes.get("modifyACL").unwrap(),
-                json_schema::Type::Type(json_schema::TypeVariant::EntityOrCommon {
+                json_schema::Type::Type { ty: json_schema::TypeVariant::EntityOrCommon {
                     type_name: "DocumentShare".parse().unwrap(),
-                }),
+                }, loc: None },
             );
             assert_has_type(
                 attributes.get("manageACL").unwrap(),
-                json_schema::Type::Type(json_schema::TypeVariant::EntityOrCommon {
+                json_schema::Type::Type { ty: json_schema::TypeVariant::EntityOrCommon {
                     type_name: "DocumentShare".parse().unwrap(),
-                }),
+                }, loc: None },
             );
         });
         let document_share = doccloud
@@ -895,13 +875,14 @@ namespace Baz {action "Foo" appliesTo {
             .entity_types
             .get(&"Resource".parse().unwrap())
             .unwrap();
-        assert_matches!(&resource.shape, json_schema::AttributesOrContext(json_schema::Type::Type(json_schema::TypeVariant::Record(json_schema::RecordType {
+        assert_matches!(&resource.shape, json_schema::AttributesOrContext(json_schema::Type::Type { ty: json_schema::TypeVariant::Record(json_schema::RecordType {
             attributes,
             additional_attributes: false,
-        }))) => {
+        }), loc: Some(_) }) => {
             assert_matches!(attributes.get("tag"), Some(json_schema::TypeOfAttribute { ty, required: true, .. }) => {
-                assert_matches!(&ty, json_schema::Type::Type(json_schema::TypeVariant::EntityOrCommon { type_name }) => {
+                assert_matches!(&ty, json_schema::Type::Type { ty: json_schema::TypeVariant::EntityOrCommon { type_name }, loc: Some(loc) } => {
                     assert_eq!(type_name, &"AWS::Tag".parse().unwrap());
+                    assert_matches!(loc.snippet(), Some("AWS::Tag"));
                 });
             });
         });
@@ -1427,21 +1408,19 @@ mod translator_tests {
             json_schema::Fragment::from_cedarschema_str(src, Extensions::all_available()).unwrap();
         let demo = frag.0.get(&Some("Demo".parse().unwrap())).unwrap();
         let user = &demo.entity_types.get(&"User".parse().unwrap()).unwrap();
-        assert_matches!(&user.shape, json_schema::AttributesOrContext(json_schema::Type::Type(json_schema::TypeVariant::Record(json_schema::RecordType {
+        assert_matches!(&user.shape, json_schema::AttributesOrContext(json_schema::Type::Type { ty: json_schema::TypeVariant::Record(json_schema::RecordType {
             attributes,
             additional_attributes: false,
-        }))) => {
+        }), loc: Some(_) }) => {
             assert_matches!(attributes.get("name"), Some(json_schema::TypeOfAttribute { ty, required: true, .. }) => {
-                let expected = json_schema::Type::Type(json_schema::TypeVariant::EntityOrCommon {
-                    type_name: "id".parse().unwrap(),
+                assert_matches!(ty, json_schema::Type::Type { ty: json_schema::TypeVariant::EntityOrCommon { type_name }, loc: Some(_) } => {
+                    assert_eq!(&type_name.to_string(), "id");
                 });
-                assert_eq!(ty, &expected);
             });
             assert_matches!(attributes.get("email"), Some(json_schema::TypeOfAttribute { ty, required: true, .. }) => {
-                let expected = json_schema::Type::Type(json_schema::TypeVariant::EntityOrCommon {
-                    type_name: "email_address".parse().unwrap(),
+                assert_matches!(ty, json_schema::Type::Type { ty: json_schema::TypeVariant::EntityOrCommon { type_name }, loc: Some(_) } => {
+                    assert_eq!(&type_name.to_string(), "email_address");
                 });
-                assert_eq!(ty, &expected);
             });
         });
         assert_matches!(ValidatorSchema::try_from(frag), Err(e) => {
@@ -2604,8 +2583,9 @@ mod entity_tags {
         assert_matches!(collect_warnings(json_schema::Fragment::from_cedarschema_str(src, Extensions::all_available())), Ok((frag, warnings)) => {
             assert!(warnings.is_empty());
             let entity_type = frag.0.get(&None).unwrap().entity_types.get(&"E".parse().unwrap()).unwrap();
-            assert_matches!(&entity_type.tags, Some(json_schema::Type::Type(json_schema::TypeVariant::EntityOrCommon { type_name })) => {
+            assert_matches!(&entity_type.tags, Some(json_schema::Type::Type { ty: json_schema::TypeVariant::EntityOrCommon { type_name }, loc: Some(loc) }) => {
                 assert_eq!(&format!("{type_name}"), "String");
+                assert_matches!(loc.snippet(), Some("String"));
             });
         });
 
@@ -2613,9 +2593,11 @@ mod entity_tags {
         assert_matches!(collect_warnings(json_schema::Fragment::from_cedarschema_str(src, Extensions::all_available())), Ok((frag, warnings)) => {
             assert!(warnings.is_empty());
             let entity_type = frag.0.get(&None).unwrap().entity_types.get(&"E".parse().unwrap()).unwrap();
-            assert_matches!(&entity_type.tags, Some(json_schema::Type::Type(json_schema::TypeVariant::Set { element })) => {
-                assert_matches!(&**element, json_schema::Type::Type(json_schema::TypeVariant::EntityOrCommon { type_name }) => {
+            assert_matches!(&entity_type.tags, Some(json_schema::Type::Type { ty: json_schema::TypeVariant::Set { element }, loc: Some(set_loc) }) => {
+                assert_matches!(&**element, json_schema::Type::Type { ty: json_schema::TypeVariant::EntityOrCommon { type_name }, loc: Some(elt_loc) } => {
                     assert_eq!(&format!("{type_name}"), "String");
+                    assert_matches!(set_loc.snippet(), Some("Set<String>"));
+                    assert_matches!(elt_loc.snippet(), Some("String"));
                 });
             });
         });
@@ -2624,10 +2606,12 @@ mod entity_tags {
         assert_matches!(collect_warnings(json_schema::Fragment::from_cedarschema_str(src, Extensions::all_available())), Ok((frag, warnings)) => {
             assert!(warnings.is_empty());
             let entity_type = frag.0.get(&None).unwrap().entity_types.get(&"E".parse().unwrap()).unwrap();
-            assert_matches!(&entity_type.tags, Some(json_schema::Type::Type(json_schema::TypeVariant::Record(rty))) => {
+            assert_matches!(&entity_type.tags, Some(json_schema::Type::Type { ty: json_schema::TypeVariant::Record(rty), loc: Some(rec_loc) }) => {
                 assert_matches!(rty.attributes.get("foo"), Some(json_schema::TypeOfAttribute { ty, required, .. }) => {
-                    assert_matches!(ty, json_schema::Type::Type(json_schema::TypeVariant::EntityOrCommon { type_name }) => {
+                    assert_matches!(ty, json_schema::Type::Type { ty: json_schema::TypeVariant::EntityOrCommon { type_name }, loc: Some(foo_loc) } => {
                         assert_eq!(&format!("{type_name}"), "String");
+                        assert_matches!(rec_loc.snippet(), Some("{ foo: String }"));
+                        assert_matches!(foo_loc.snippet(), Some("String"));
                     });
                     assert!(*required);
                 });
@@ -2638,8 +2622,9 @@ mod entity_tags {
         assert_matches!(collect_warnings(json_schema::Fragment::from_cedarschema_str(src, Extensions::all_available())), Ok((frag, warnings)) => {
             assert!(warnings.is_empty());
             let entity_type = frag.0.get(&None).unwrap().entity_types.get(&"E".parse().unwrap()).unwrap();
-            assert_matches!(&entity_type.tags, Some(json_schema::Type::Type(json_schema::TypeVariant::EntityOrCommon { type_name })) => {
+            assert_matches!(&entity_type.tags, Some(json_schema::Type::Type { ty: json_schema::TypeVariant::EntityOrCommon { type_name }, loc: Some(loc) }) => {
                 assert_eq!(&format!("{type_name}"), "T");
+                assert_matches!(loc.snippet(), Some("T"));
             });
         });
 
@@ -2647,8 +2632,9 @@ mod entity_tags {
         assert_matches!(collect_warnings(json_schema::Fragment::from_cedarschema_str(src, Extensions::all_available())), Ok((frag, warnings)) => {
             assert!(warnings.is_empty());
             let entity_type = frag.0.get(&None).unwrap().entity_types.get(&"E".parse().unwrap()).unwrap();
-            assert_matches!(&entity_type.tags, Some(json_schema::Type::Type(json_schema::TypeVariant::EntityOrCommon { type_name })) => {
+            assert_matches!(&entity_type.tags, Some(json_schema::Type::Type { ty: json_schema::TypeVariant::EntityOrCommon { type_name }, loc: Some(loc) }) => {
                 assert_eq!(&format!("{type_name}"), "E");
+                assert_matches!(loc.snippet(), Some("E"));
             });
         });
     }
@@ -2738,7 +2724,7 @@ entity User {
             parse_schema(
                 r#"
         @doc("This entity defines our central user type")
-entity User { 
+entity User {
     manager : User,
     team : String
 };
@@ -2753,7 +2739,7 @@ entity User {
         @doc("this is namespace foo")
         namespace foo {
 @doc("This entity defines our central user type")
-entity User { 
+entity User {
     manager : User,
     team : String
 };
@@ -2767,10 +2753,10 @@ entity User {
             parse_schema(
                 r#"
 @doc("This entity defines our central user type")
-entity User { 
+entity User {
     manager : User,
 
-    @doc("Which team user belongs to")    
+    @doc("Which team user belongs to")
     @docLink("https://schemaDocs.example.com/User/team")
     team : String
 };