Repository: cedar-policy/cedar
Pull request #1533 (2025-03-12 20:28:07)

Include source location in schema entity types and action types
## Description of changes

Include source locations in entity types and action types in schema. This allows us to give more informative error messages when validating policies and can be useful as part of an LSP/IDE plugin

## Issue #, if available

## Checklist for requesting a review

The change in this PR is (choose one, and delete the other options):

- [ ] A breaking change requiring a major version bump to `cedar-policy` (e.g., changes to the signature of an existing API).
- [ ] A backwards-compatible change requiring a minor version bump to `cedar-policy` (e.g., addition of a new API).
- [ ] A bug fix or other functionality change requiring a patch to `cedar-policy`.
- [ x ] A change "invisible" to users (e.g., documentation, changes to "internal" crates like `cedar-policy-core`, `cedar-validator`, etc.)
- [ ] A change (breaking or otherwise) that only impacts unreleased or experimental code.

I confirm that this PR (choose one, and delete the other options):

- [ ] Updates the "Unreleased" section of the CHANGELOG with a description of my change (required for major/minor version bumps).
- [ 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 ] Does not require updates because my change does not impact the Cedar formal model or DRT infrastructure.
- [ ] 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.)
- [ ] Requires updates, but I do not plan to make them in the near future. (Make sure that your changes are hidden behind a feature flag to mark them as experimental.)
- [ ] I'm not sure how my change impacts `cedar-spec`. (Post your PR anyways, and we'll discuss in the comments.)

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.
- [ ] 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-docs`](https://github.com/cedar-policy/cedar-docs). PRs should be targeted at a `staging-X.Y` branch, not `main`.)
- [ ] I'm not sure how my change impacts the documentation. (Post your PR anyways, and we'll discuss in the comments.)


github-actions[bot]: 
# Coverage Report
Head Commit: ```5662fa6307fe386c8a18954afc78450ee93a7425```

Base Commit: ```d2ab99af3825f233991e8e68e4434c8996f4bcd0```

[Download the full coverage report.](https://github.com/cedar-policy/cedar/actions/runs/13840283718/artifacts/2747310055)

## Coverage of Added or Modified Lines of Rust Code
**Required coverage:** 80.00%

**Actual coverage:** 87.00%

**Status:** PASSED :white_check_mark:

<details>
<summary><b>Details</b></summary>

| File | Status | Covered | Coverage | Missed Lines |
|:-----|:------:|:-------:|---------:|:-------------|
cedar-policy-core/src/ast/entity.rs | :green_circle: | 11/11 | 100.00% | 
cedar-policy-core/src/ast/id.rs | :green_circle: | 3/3 | 100.00% | 
cedar-policy-core/src/ast/name.rs | :green_circle: | 5/5 | 100.00% | 
cedar-policy-validator/src/cedar_schema/fmt.rs | :green_circle: | 2/2 | 100.00% | 
cedar-policy-validator/src/cedar_schema/to_json_schema.rs | :green_circle: | 16/16 | 100.00% | 
cedar-policy-validator/src/json_schema.rs | :red_circle: | 9/14 | 64.29% | 646, 689, 847, 879, 1162
cedar-policy-validator/src/schema.rs | :green_circle: | 11/11 | 100.00% | 
cedar-policy-validator/src/schema/action.rs | :red_circle: | 0/5 | 0.00% | 79, 91, 101-103
cedar-policy-validator/src/schema/entity_type.rs | :green_circle: | 4/4 | 100.00% | 
cedar-policy-validator/src/schema/namespace_def.rs | :green_circle: | 22/22 | 100.00% | 
cedar-policy-validator/src/schema/raw_name.rs | :green_circle: | 4/4 | 100.00% | 
cedar-policy/src/proto/validator.rs | :red_circle: | 0/3 | 0.00% | 126, 170, 177
</details>

## Coverage of All Lines of Rust Code
**Required coverage:** 80.00%

**Actual coverage:** 81.51%

**Status:** PASSED :white_check_mark:

<details>
<summary><b>Details</b></summary>

| Package | Status | Covered | Coverage | Base Coverage |
|:--------|:------:|:-------:|---------:|--------------:|
cedar-policy | :red_circle: | 3059/4397 | 69.57% | 69.60%
cedar-policy-cli | :red_circle: | 538/927 | 58.04% | 58.04%
cedar-policy-core | :green_circle: | 11652/13894 | 83.86% | 83.85%
cedar-policy-formatter | :green_circle: | 914/1043 | 87.63% | 87.63%
cedar-policy-validator | :green_circle: | 8282/9701 | 85.37% | 85.41%
cedar-wasm | :red_circle: | 0/29 | 0.00% | 0.00%
</details>

github-actions[bot]: 
# Coverage Report
Head Commit: ```7b20e6260cafd9492a5aab2451083b3572c2ec3f```

Base Commit: ```244ba36c5dfda5abb0a58f7b3f83d7a0b105b176```

[Download the full coverage report.](https://github.com/cedar-policy/cedar/actions/runs/13840296361/artifacts/2747318983)

## Coverage of Added or Modified Lines of Rust Code
**Required coverage:** 80.00%

**Actual coverage:** 87.00%

**Status:** PASSED :white_check_mark:

<details>
<summary><b>Details</b></summary>

| File | Status | Covered | Coverage | Missed Lines |
|:-----|:------:|:-------:|---------:|:-------------|
cedar-policy-core/src/ast/entity.rs | :green_circle: | 11/11 | 100.00% | 
cedar-policy-core/src/ast/id.rs | :green_circle: | 3/3 | 100.00% | 
cedar-policy-core/src/ast/name.rs | :green_circle: | 5/5 | 100.00% | 
cedar-policy-validator/src/cedar_schema/fmt.rs | :green_circle: | 2/2 | 100.00% | 
cedar-policy-validator/src/cedar_schema/to_json_schema.rs | :green_circle: | 16/16 | 100.00% | 
cedar-policy-validator/src/json_schema.rs | :red_circle: | 9/14 | 64.29% | 646, 689, 847, 879, 1162
cedar-policy-validator/src/schema.rs | :green_circle: | 11/11 | 100.00% | 
cedar-policy-validator/src/schema/action.rs | :red_circle: | 0/5 | 0.00% | 79, 91, 101-103
cedar-policy-validator/src/schema/entity_type.rs | :green_circle: | 4/4 | 100.00% | 
cedar-policy-validator/src/schema/namespace_def.rs | :green_circle: | 22/22 | 100.00% | 
cedar-policy-validator/src/schema/raw_name.rs | :green_circle: | 4/4 | 100.00% | 
cedar-policy/src/proto/validator.rs | :red_circle: | 0/3 | 0.00% | 126, 170, 177
</details>

## Coverage of All Lines of Rust Code
**Required coverage:** 80.00%

**Actual coverage:** 81.50%

**Status:** PASSED :white_check_mark:

<details>
<summary><b>Details</b></summary>

| Package | Status | Covered | Coverage | Base Coverage |
|:--------|:------:|:-------:|---------:|--------------:|
cedar-policy | :red_circle: | 3059/4397 | 69.57% | --
cedar-policy-cli | :red_circle: | 538/927 | 58.04% | --
cedar-policy-core | :green_circle: | 11651/13894 | 83.86% | --
cedar-policy-formatter | :green_circle: | 914/1043 | 87.63% | --
cedar-policy-validator | :green_circle: | 8282/9701 | 85.37% | --
cedar-wasm | :red_circle: | 0/29 | 0.00% | --
</details>

github-actions[bot]: 
# Coverage Report
Head Commit: ```746838c3cd7a202a9f92bc9d4cde955f3e6e4975```

Base Commit: ```244ba36c5dfda5abb0a58f7b3f83d7a0b105b176```

[Download the full coverage report.](https://github.com/cedar-policy/cedar/actions/runs/13857822706/artifacts/2752997753)

## Coverage of Added or Modified Lines of Rust Code
**Required coverage:** 80.00%

**Actual coverage:** 89.81%

**Status:** PASSED :white_check_mark:

<details>
<summary><b>Details</b></summary>

| File | Status | Covered | Coverage | Missed Lines |
|:-----|:------:|:-------:|---------:|:-------------|
cedar-policy-core/src/ast/entity.rs | :green_circle: | 11/11 | 100.00% | 
cedar-policy-core/src/ast/id.rs | :green_circle: | 3/3 | 100.00% | 
cedar-policy-core/src/ast/name.rs | :green_circle: | 5/5 | 100.00% | 
cedar-policy-validator/src/cedar_schema/fmt.rs | :green_circle: | 2/2 | 100.00% | 
cedar-policy-validator/src/cedar_schema/to_json_schema.rs | :green_circle: | 12/12 | 100.00% | 
cedar-policy-validator/src/json_schema.rs | :yellow_circle: | 8/11 | 72.73% | 840, 872, 1155
cedar-policy-validator/src/schema.rs | :green_circle: | 11/11 | 100.00% | 
cedar-policy-validator/src/schema/action.rs | :red_circle: | 0/5 | 0.00% | 79, 91, 101-103
cedar-policy-validator/src/schema/entity_type.rs | :green_circle: | 4/4 | 100.00% | 
cedar-policy-validator/src/schema/namespace_def.rs | :green_circle: | 37/37 | 100.00% | 
cedar-policy-validator/src/schema/raw_name.rs | :green_circle: | 4/4 | 100.00% | 
cedar-policy/src/proto/validator.rs | :red_circle: | 0/3 | 0.00% | 126, 170, 177
</details>

## Coverage of All Lines of Rust Code
**Required coverage:** 80.00%

**Actual coverage:** 81.51%

**Status:** PASSED :white_check_mark:

<details>
<summary><b>Details</b></summary>

| Package | Status | Covered | Coverage | Base Coverage |
|:--------|:------:|:-------:|---------:|--------------:|
cedar-policy | :red_circle: | 3059/4397 | 69.57% | 69.60%
cedar-policy-cli | :red_circle: | 538/927 | 58.04% | 58.04%
cedar-policy-core | :green_circle: | 11652/13894 | 83.86% | 83.85%
cedar-policy-formatter | :green_circle: | 914/1043 | 87.63% | 87.63%
cedar-policy-validator | :green_circle: | 8276/9692 | 85.39% | 85.41%
cedar-wasm | :red_circle: | 0/29 | 0.00% | 0.00%
</details>

github-actions[bot]: 
# Coverage Report
Head Commit: ```900607e2512454eb81b5a677a1719f535f3aeb1a```

Base Commit: ```244ba36c5dfda5abb0a58f7b3f83d7a0b105b176```

[Download the full coverage report.](https://github.com/cedar-policy/cedar/actions/runs/13859131341/artifacts/2753485382)

## Coverage of Added or Modified Lines of Rust Code
**Required coverage:** 80.00%

**Actual coverage:** 89.80%

**Status:** PASSED :white_check_mark:

<details>
<summary><b>Details</b></summary>

| File | Status | Covered | Coverage | Missed Lines |
|:-----|:------:|:-------:|---------:|:-------------|
cedar-policy-core/src/ast/entity.rs | :green_circle: | 11/11 | 100.00% | 
cedar-policy-core/src/ast/id.rs | :green_circle: | 3/3 | 100.00% | 
cedar-policy-core/src/ast/name.rs | :green_circle: | 5/5 | 100.00% | 
cedar-policy-validator/src/cedar_schema/fmt.rs | :green_circle: | 2/2 | 100.00% | 
cedar-policy-validator/src/cedar_schema/to_json_schema.rs | :green_circle: | 13/13 | 100.00% | 
cedar-policy-validator/src/json_schema.rs | :green_circle: | 27/33 | 81.82% | 841-842, 874-875, 1186-1187
cedar-policy-validator/src/schema.rs | :green_circle: | 11/11 | 100.00% | 
cedar-policy-validator/src/schema/action.rs | :red_circle: | 0/5 | 0.00% | 79, 91, 101-103
cedar-policy-validator/src/schema/entity_type.rs | :green_circle: | 4/4 | 100.00% | 
cedar-policy-validator/src/schema/namespace_def.rs | :green_circle: | 52/53 | 98.11% | 672
cedar-policy-validator/src/schema/raw_name.rs | :green_circle: | 4/4 | 100.00% | 
cedar-policy/src/proto/validator.rs | :red_circle: | 0/3 | 0.00% | 126, 170, 177
</details>

## Coverage of All Lines of Rust Code
**Required coverage:** 80.00%

**Actual coverage:** 81.53%

**Status:** PASSED :white_check_mark:

<details>
<summary><b>Details</b></summary>

| Package | Status | Covered | Coverage | Base Coverage |
|:--------|:------:|:-------:|---------:|--------------:|
cedar-policy | :red_circle: | 3059/4397 | 69.57% | 69.60%
cedar-policy-cli | :red_circle: | 538/927 | 58.04% | 58.04%
cedar-policy-core | :green_circle: | 11652/13894 | 83.86% | 83.85%
cedar-policy-formatter | :green_circle: | 914/1043 | 87.63% | 87.63%
cedar-policy-validator | :green_circle: | 8311/9730 | 85.42% | 85.41%
cedar-wasm | :red_circle: | 0/29 | 0.00% | 0.00%
</details>

github-actions[bot]: 
# Coverage Report
Head Commit: ```4da41c0294457c5af244d7b7f665aadb6b877983```

Base Commit: ```244ba36c5dfda5abb0a58f7b3f83d7a0b105b176```

[Download the full coverage report.](https://github.com/cedar-policy/cedar/actions/runs/13859488250/artifacts/2753621918)

## Coverage of Added or Modified Lines of Rust Code
**Required coverage:** 80.00%

**Actual coverage:** 89.80%

**Status:** PASSED :white_check_mark:

<details>
<summary><b>Details</b></summary>

| File | Status | Covered | Coverage | Missed Lines |
|:-----|:------:|:-------:|---------:|:-------------|
cedar-policy-core/src/ast/entity.rs | :green_circle: | 11/11 | 100.00% | 
cedar-policy-core/src/ast/id.rs | :green_circle: | 3/3 | 100.00% | 
cedar-policy-core/src/ast/name.rs | :green_circle: | 5/5 | 100.00% | 
cedar-policy-validator/src/cedar_schema/fmt.rs | :green_circle: | 2/2 | 100.00% | 
cedar-policy-validator/src/cedar_schema/to_json_schema.rs | :green_circle: | 13/13 | 100.00% | 
cedar-policy-validator/src/json_schema.rs | :green_circle: | 27/33 | 81.82% | 841-842, 874-875, 1186-1187
cedar-policy-validator/src/schema.rs | :green_circle: | 11/11 | 100.00% | 
cedar-policy-validator/src/schema/action.rs | :red_circle: | 0/5 | 0.00% | 79, 91, 101-103
cedar-policy-validator/src/schema/entity_type.rs | :green_circle: | 4/4 | 100.00% | 
cedar-policy-validator/src/schema/namespace_def.rs | :green_circle: | 52/53 | 98.11% | 672
cedar-policy-validator/src/schema/raw_name.rs | :green_circle: | 4/4 | 100.00% | 
cedar-policy/src/proto/validator.rs | :red_circle: | 0/3 | 0.00% | 126, 170, 177
</details>

## Coverage of All Lines of Rust Code
**Required coverage:** 80.00%

**Actual coverage:** 81.53%

**Status:** PASSED :white_check_mark:

<details>
<summary><b>Details</b></summary>

| Package | Status | Covered | Coverage | Base Coverage |
|:--------|:------:|:-------:|---------:|--------------:|
cedar-policy | :red_circle: | 3059/4397 | 69.57% | 69.60%
cedar-policy-cli | :red_circle: | 538/927 | 58.04% | 58.04%
cedar-policy-core | :green_circle: | 11652/13894 | 83.86% | 83.85%
cedar-policy-formatter | :green_circle: | 914/1043 | 87.63% | 87.63%
cedar-policy-validator | :green_circle: | 8311/9730 | 85.42% | 85.41%
cedar-wasm | :red_circle: | 0/29 | 0.00% | 0.00%
</details>

github-actions[bot]: 
# Coverage Report
Head Commit: ```d13ebec31574d9cf7fbec8f10da9b5ef85f73c9c```

Base Commit: ```244ba36c5dfda5abb0a58f7b3f83d7a0b105b176```

[Download the full coverage report.](https://github.com/cedar-policy/cedar/actions/runs/13859536690/artifacts/2753638060)

## Coverage of Added or Modified Lines of Rust Code
**Required coverage:** 80.00%

**Actual coverage:** 89.80%

**Status:** PASSED :white_check_mark:

<details>
<summary><b>Details</b></summary>

| File | Status | Covered | Coverage | Missed Lines |
|:-----|:------:|:-------:|---------:|:-------------|
cedar-policy-core/src/ast/entity.rs | :green_circle: | 11/11 | 100.00% | 
cedar-policy-core/src/ast/id.rs | :green_circle: | 3/3 | 100.00% | 
cedar-policy-core/src/ast/name.rs | :green_circle: | 5/5 | 100.00% | 
cedar-policy-validator/src/cedar_schema/fmt.rs | :green_circle: | 2/2 | 100.00% | 
cedar-policy-validator/src/cedar_schema/to_json_schema.rs | :green_circle: | 13/13 | 100.00% | 
cedar-policy-validator/src/json_schema.rs | :green_circle: | 27/33 | 81.82% | 841-842, 874-875, 1186-1187
cedar-policy-validator/src/schema.rs | :green_circle: | 11/11 | 100.00% | 
cedar-policy-validator/src/schema/action.rs | :red_circle: | 0/5 | 0.00% | 79, 91, 101-103
cedar-policy-validator/src/schema/entity_type.rs | :green_circle: | 4/4 | 100.00% | 
cedar-policy-validator/src/schema/namespace_def.rs | :green_circle: | 52/53 | 98.11% | 672
cedar-policy-validator/src/schema/raw_name.rs | :green_circle: | 4/4 | 100.00% | 
cedar-policy/src/proto/validator.rs | :red_circle: | 0/3 | 0.00% | 126, 170, 177
</details>

## Coverage of All Lines of Rust Code
**Required coverage:** 80.00%

**Actual coverage:** 81.53%

**Status:** PASSED :white_check_mark:

<details>
<summary><b>Details</b></summary>

| Package | Status | Covered | Coverage | Base Coverage |
|:--------|:------:|:-------:|---------:|--------------:|
cedar-policy | :red_circle: | 3059/4397 | 69.57% | 69.60%
cedar-policy-cli | :red_circle: | 538/927 | 58.04% | 58.04%
cedar-policy-core | :green_circle: | 11652/13894 | 83.86% | 83.85%
cedar-policy-formatter | :green_circle: | 914/1043 | 87.63% | 87.63%
cedar-policy-validator | :green_circle: | 8311/9730 | 85.42% | 85.41%
cedar-wasm | :red_circle: | 0/29 | 0.00% | 0.00%
</details>

github-actions[bot]: 
# Coverage Report
Head Commit: ```e66978c0691e92017430a91e79323a6301546da4```

Base Commit: ```e46cb111fdf3566a0dfc33fe21d8adccb900530f```

[Download the full coverage report.](https://github.com/cedar-policy/cedar/actions/runs/13923393275/artifacts/2772412316)

## Coverage of Added or Modified Lines of Rust Code
**Required coverage:** 80.00%

**Actual coverage:** 89.80%

**Status:** PASSED :white_check_mark:

<details>
<summary><b>Details</b></summary>

| File | Status | Covered | Coverage | Missed Lines |
|:-----|:------:|:-------:|---------:|:-------------|
cedar-policy-core/src/ast/entity.rs | :green_circle: | 11/11 | 100.00% | 
cedar-policy-core/src/ast/id.rs | :green_circle: | 3/3 | 100.00% | 
cedar-policy-core/src/ast/name.rs | :green_circle: | 5/5 | 100.00% | 
cedar-policy-validator/src/cedar_schema/fmt.rs | :green_circle: | 2/2 | 100.00% | 
cedar-policy-validator/src/cedar_schema/to_json_schema.rs | :green_circle: | 13/13 | 100.00% | 
cedar-policy-validator/src/json_schema.rs | :green_circle: | 27/33 | 81.82% | 841-842, 874-875, 1186-1187
cedar-policy-validator/src/schema.rs | :green_circle: | 11/11 | 100.00% | 
cedar-policy-validator/src/schema/action.rs | :red_circle: | 0/5 | 0.00% | 79, 91, 101-103
cedar-policy-validator/src/schema/entity_type.rs | :green_circle: | 4/4 | 100.00% | 
cedar-policy-validator/src/schema/namespace_def.rs | :green_circle: | 52/53 | 98.11% | 672
cedar-policy-validator/src/schema/raw_name.rs | :green_circle: | 4/4 | 100.00% | 
cedar-policy/src/proto/validator.rs | :red_circle: | 0/3 | 0.00% | 126, 170, 177
</details>

## Coverage of All Lines of Rust Code
**Required coverage:** 80.00%

**Actual coverage:** 81.53%

**Status:** PASSED :white_check_mark:

<details>
<summary><b>Details</b></summary>

| Package | Status | Covered | Coverage | Base Coverage |
|:--------|:------:|:-------:|---------:|--------------:|
cedar-policy | :red_circle: | 3059/4397 | 69.57% | 69.60%
cedar-policy-cli | :red_circle: | 538/927 | 58.04% | 58.04%
cedar-policy-core | :green_circle: | 11657/13899 | 83.87% | 83.85%
cedar-policy-formatter | :green_circle: | 914/1043 | 87.63% | 87.63%
cedar-policy-validator | :green_circle: | 8311/9730 | 85.42% | 85.41%
cedar-wasm | :red_circle: | 0/29 | 0.00% | 0.00%
</details>

eidelmanjonathan: > Is there a reason this needs the `extended-schema` feature? It does makes sense as a way to be absolutely certain we're not changing the JSON schema format, but I'm wondering if there's some other reason we need it.

I added in the `extended-schema` because we were failing the DRT build because I added the `defn_loc` to the `ActionType`. That said, there will be a subsequent, more substantial PR coming after this which will store the locations of common types in the `ValidatorSchema` and I think the feature flag is definitely justified for that. So overall I think it is worth having.

Patch:
diff --git a/cedar-policy-core/src/ast/entity.rs b/cedar-policy-core/src/ast/entity.rs
index 22b83822..739877a9 100644
--- a/cedar-policy-core/src/ast/entity.rs
+++ b/cedar-policy-core/src/ast/entity.rs
@@ -119,6 +119,19 @@ impl EntityType {
         }
     }
 
+    /// Create a clone of this EntityType with given loc
+    pub fn with_loc(&self, loc: Option<&Loc>) -> Self {
+        match self {
+            EntityType::EntityType(name) => EntityType::EntityType(Name(InternalName {
+                id: name.0.id.clone(),
+                path: name.0.path.clone(),
+                loc: loc.cloned(),
+            })),
+            #[cfg(feature = "tolerant-ast")]
+            EntityType::ErrorEntityType => self.clone(),
+        }
+    }
+
     /// Calls [`Name::qualify_with_name`] on the underlying [`Name`]
     pub fn qualify_with(&self, namespace: Option<&Name>) -> Self {
         match self {
diff --git a/cedar-policy-core/src/ast/id.rs b/cedar-policy-core/src/ast/id.rs
index 5dc3c5f9..e1b19267 100644
--- a/cedar-policy-core/src/ast/id.rs
+++ b/cedar-policy-core/src/ast/id.rs
@@ -106,7 +106,9 @@ impl TryFrom<Id> for UnreservedId {
     type Error = ReservedNameError;
     fn try_from(value: Id) -> Result<Self, Self::Error> {
         if value.is_reserved() {
-            Err(ReservedNameError(InternalName::unqualified_name(value)))
+            Err(ReservedNameError(InternalName::unqualified_name(
+                value, None,
+            )))
         } else {
             Ok(Self(value))
         }
diff --git a/cedar-policy-core/src/ast/name.rs b/cedar-policy-core/src/ast/name.rs
index 9af81854..ddf61835 100644
--- a/cedar-policy-core/src/ast/name.rs
+++ b/cedar-policy-core/src/ast/name.rs
@@ -53,7 +53,7 @@ pub struct InternalName {
 /// A shortcut for [`InternalName::unqualified_name`]
 impl From<Id> for InternalName {
     fn from(value: Id) -> Self {
-        Self::unqualified_name(value)
+        Self::unqualified_name(value, None)
     }
 }
 
@@ -82,18 +82,18 @@ impl InternalName {
     }
 
     /// Create an [`InternalName`] with no path (no namespaces).
-    pub fn unqualified_name(id: Id) -> Self {
+    pub fn unqualified_name(id: Id, loc: Option<Loc>) -> Self {
         Self {
             id,
             path: Arc::new(vec![]),
-            loc: None,
+            loc,
         }
     }
 
     /// Get the [`InternalName`] representing the reserved `__cedar` namespace
     pub fn __cedar() -> Self {
         // using `Id::new_unchecked()` for performance reasons -- this function is called many times by validator code
-        Self::unqualified_name(Id::new_unchecked("__cedar"))
+        Self::unqualified_name(Id::new_unchecked("__cedar"), None)
     }
 
     /// Create an [`InternalName`] with no path (no namespaces).
@@ -444,7 +444,7 @@ impl Name {
     /// Create a [`Name`] with no path (no namespaces).
     pub fn unqualified_name(id: UnreservedId) -> Self {
         // This is safe (upholds the `Name` invariant) because `id` must be an `UnreservedId`
-        Self(InternalName::unqualified_name(id.0))
+        Self(InternalName::unqualified_name(id.0, None))
     }
 
     /// Get the basename of the [`Name`] (ie, with namespaces stripped).
diff --git a/cedar-policy-validator/Cargo.toml b/cedar-policy-validator/Cargo.toml
index 02038d3f..c9f62bb2 100644
--- a/cedar-policy-validator/Cargo.toml
+++ b/cedar-policy-validator/Cargo.toml
@@ -51,7 +51,8 @@ level-validate = []
 wasm = ["serde-wasm-bindgen", "tsify", "wasm-bindgen"]
 entity-manifest = []
 tolerant-ast = ["cedar-policy-core/tolerant-ast"]
-experimental = ["tolerant-ast"]
+experimental = ["tolerant-ast", "extended-schema"]
+extended-schema = []
 
 [dev-dependencies]
 similar-asserts = "1.7.0"
diff --git a/cedar-policy-validator/src/cedar_schema/fmt.rs b/cedar-policy-validator/src/cedar_schema/fmt.rs
index bd72ce5e..de1d5b5b 100644
--- a/cedar-policy-validator/src/cedar_schema/fmt.rs
+++ b/cedar-policy-validator/src/cedar_schema/fmt.rs
@@ -231,14 +231,14 @@ pub fn json_schema_to_cedar_schema_str<N: Display>(
             .entity_types
             .keys()
             .map(|ty_name| {
-                RawName::new_from_unreserved(ty_name.clone()).qualify_with_name(name.as_ref())
+                RawName::new_from_unreserved(ty_name.clone(), None).qualify_with_name(name.as_ref())
             })
             .collect();
         let common_types: HashSet<InternalName> = ns
             .common_types
             .keys()
             .map(|ty_name| {
-                RawName::new_from_unreserved(ty_name.clone().into())
+                RawName::new_from_unreserved(ty_name.clone().into(), None)
                     .qualify_with_name(name.as_ref())
             })
             .collect();
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 1b6e7c02..e20b09dd 100644
--- a/cedar-policy-validator/src/cedar_schema/to_json_schema.rs
+++ b/cedar-policy-validator/src/cedar_schema/to_json_schema.rs
@@ -236,15 +236,19 @@ fn convert_action_decl(
             context: json_schema::AttributesOrContext::default(),
         });
     let member_of = parents.map(|parents| parents.into_iter().map(convert_qual_name).collect());
-    let ty = json_schema::ActionType {
-        attributes: None, // Action attributes are currently unsupported in the Cedar schema format
-        applies_to: Some(applies_to),
-        member_of,
-        annotations: a.annotations.into(),
-        loc: Some(a.data.loc),
-    };
-    // Then map that type across all of the bound names
-    Ok(names.into_iter().map(move |name| (name.node, ty.clone())))
+
+    Ok(names.into_iter().map(move |name| {
+        let ty = json_schema::ActionType {
+            attributes: None, // Action attributes are currently unsupported in the Cedar schema format
+            applies_to: Some(applies_to.clone()),
+            member_of: member_of.clone(),
+            annotations: a.annotations.clone().into(),
+            loc: Some(a.data.loc.clone()),
+            #[cfg(feature = "extended-schema")]
+            defn_loc: Some(name.loc),
+        };
+        (name.node, ty)
+    }))
 }
 
 fn convert_qual_name(qn: Node<QualName>) -> json_schema::ActionEntityUID<RawName> {
diff --git a/cedar-policy-validator/src/json_schema.rs b/cedar-policy-validator/src/json_schema.rs
index 3c1d5323..d4dab011 100644
--- a/cedar-policy-validator/src/json_schema.rs
+++ b/cedar-policy-validator/src/json_schema.rs
@@ -34,6 +34,7 @@ use serde::{
 };
 use serde_with::serde_as;
 use smol_str::{SmolStr, ToSmolStr};
+use std::hash::Hash;
 use std::{
     collections::{BTreeMap, HashMap, HashSet},
     fmt::Display,
@@ -576,7 +577,8 @@ impl<'de, N: Deserialize<'de> + From<RawName>> Deserialize<'de> for EntityType<N
 
 /// The "standard" entity type. That is, an entity type defined by parent
 /// entity types, shape, and tags.
-#[derive(Debug, Clone, Serialize, PartialEq, Eq, Deserialize)]
+#[derive(Debug, Clone, Serialize, Deserialize, Educe)]
+#[educe(PartialEq, Eq)]
 #[serde(bound(deserialize = "N: Deserialize<'de> + From<RawName>"))]
 #[serde(rename_all = "camelCase")]
 #[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
@@ -802,7 +804,7 @@ pub struct ActionType<N> {
     #[serde(default)]
     #[serde(skip_serializing_if = "Annotations::is_empty")]
     pub annotations: Annotations,
-    /// Source location
+    /// Source location of the whole type
     ///
     /// (As of this writing, this is not populated when parsing from JSON.
     /// It is only populated if constructing this structure from the
@@ -810,6 +812,12 @@ pub struct ActionType<N> {
     #[serde(skip)]
     #[educe(PartialEq(ignore))]
     pub loc: Option<Loc>,
+
+    /// Source location of only the action definition
+    #[cfg(feature = "extended-schema")]
+    #[serde(skip)]
+    #[educe(PartialEq(ignore))]
+    pub(crate) defn_loc: Option<Loc>,
 }
 
 impl ActionType<RawName> {
@@ -830,6 +838,8 @@ impl ActionType<RawName> {
             }),
             annotations: self.annotations,
             loc: self.loc,
+            #[cfg(feature = "extended-schema")]
+            defn_loc: self.defn_loc,
         }
     }
 }
@@ -861,6 +871,8 @@ impl ActionType<ConditionalName> {
                 .transpose()?,
             annotations: self.annotations,
             loc: self.loc,
+            #[cfg(feature = "extended-schema")]
+            defn_loc: self.defn_loc,
         })
     }
 }
@@ -967,13 +979,22 @@ pub struct ActionEntityUID<N> {
     #[serde(default)]
     #[serde(skip_serializing_if = "Option::is_none")]
     ty: Option<N>,
+    #[cfg(feature = "extended-schema")]
+    #[serde(skip)]
+    /// Source location - if available
+    pub loc: Option<Loc>,
 }
 
 impl ActionEntityUID<RawName> {
     /// Create a new `ActionEntityUID<RawName>`.
     /// `ty` = `None` is shorthand for `Action`.
     pub fn new(ty: Option<RawName>, id: SmolStr) -> Self {
-        Self { id, ty }
+        Self {
+            id,
+            ty,
+            #[cfg(feature = "extended-schema")]
+            loc: None,
+        }
     }
 
     /// Given an `id`, get the [`ActionEntityUID`] representing `Action::<id>`.
@@ -981,7 +1002,21 @@ impl ActionEntityUID<RawName> {
     // This function is only available for `RawName` and not other values of `N`,
     // in order to uphold the INVARIANT on self.ty.
     pub fn default_type(id: SmolStr) -> Self {
-        Self { id, ty: None }
+        Self {
+            id,
+            ty: None,
+            #[cfg(feature = "extended-schema")]
+            loc: None,
+        }
+    }
+
+    /// Given an `id`, get the [`ActionEntityUID`] representing `Action::<id>`.
+    //
+    // This function is only available for `RawName` and not other values of `N`,
+    // in order to uphold the INVARIANT on self.ty.
+    #[cfg(feature = "extended-schema")]
+    pub fn default_type_with_loc(id: SmolStr, loc: Option<Loc>) -> Self {
+        Self { id, ty: None, loc }
     }
 }
 
@@ -1014,6 +1049,8 @@ impl ActionEntityUID<RawName> {
                     .unwrap_or_else(|| RawName::from_str("Action").expect("valid raw name"));
                 Some(raw_name.conditionally_qualify_with(ns, ReferenceType::Entity))
             },
+            #[cfg(feature = "extended-schema")]
+            loc: None,
         }
     }
 
@@ -1031,6 +1068,8 @@ impl ActionEntityUID<RawName> {
                     .unwrap_or_else(|| RawName::from_str("Action").expect("valid raw name"));
                 Some(raw_name.qualify_with(ns))
             },
+            #[cfg(feature = "extended-schema")]
+            loc: self.loc,
         }
     }
 }
@@ -1078,6 +1117,8 @@ impl ActionEntityUID<ConditionalName> {
             .map(|possibility| ActionEntityUID {
                 id: self.id.clone(),
                 ty: Some(possibility.clone()),
+                #[cfg(feature = "extended-schema")]
+                loc: None,
             })
     }
 
@@ -1088,6 +1129,8 @@ impl ActionEntityUID<ConditionalName> {
         ActionEntityUID {
             id: self.id.clone(),
             ty: self.ty.as_ref().map(|ty| ty.raw().clone()),
+            #[cfg(feature = "extended-schema")]
+            loc: None,
         }
     }
 }
@@ -1122,10 +1165,14 @@ impl TryFrom<ActionEntityUID<InternalName>> for EntityUID {
         aeuid: ActionEntityUID<InternalName>,
     ) -> std::result::Result<Self, <InternalName as TryInto<Name>>::Error> {
         let ty = Name::try_from(aeuid.ty().clone())?;
+        #[cfg(feature = "extended-schema")]
+        let loc = aeuid.loc;
+        #[cfg(not(feature = "extended-schema"))]
+        let loc = None;
         Ok(EntityUID::from_components(
             ty.into(),
             Eid::new(aeuid.id),
-            None,
+            loc,
         ))
     }
 }
@@ -1136,6 +1183,8 @@ impl From<EntityUID> for ActionEntityUID<Name> {
         ActionEntityUID {
             ty: Some(ty.into()),
             id: <Eid as AsRef<SmolStr>>::as_ref(&id).clone(),
+            #[cfg(feature = "extended-schema")]
+            loc: None,
         }
     }
 }
@@ -2209,7 +2258,9 @@ mod test {
             at.member_of,
             Some(vec![ActionEntityUID {
                 ty: None,
-                id: "readWrite".into()
+                id: "readWrite".into(),
+                #[cfg(feature = "extended-schema")]
+                loc: None
             }])
         );
     }
@@ -3146,6 +3197,8 @@ mod test_json_roundtrip {
                         member_of: None,
                         annotations: Annotations::new(),
                         loc: None,
+                        #[cfg(feature = "extended-schema")]
+                        defn_loc: None,
                     },
                 )],
             ),
@@ -3202,6 +3255,8 @@ mod test_json_roundtrip {
                             member_of: None,
                             annotations: Annotations::new(),
                             loc: None,
+                            #[cfg(feature = "extended-schema")]
+                            defn_loc: None,
                         },
                     )],
                 ),
diff --git a/cedar-policy-validator/src/lib.rs b/cedar-policy-validator/src/lib.rs
index cbaf7a18..d9191f33 100644
--- a/cedar-policy-validator/src/lib.rs
+++ b/cedar-policy-validator/src/lib.rs
@@ -294,6 +294,8 @@ mod test {
                     attributes: None,
                     annotations: Annotations::new(),
                     loc: None,
+                    #[cfg(feature = "extended-schema")]
+                    defn_loc: None,
                 },
             )],
         );
diff --git a/cedar-policy-validator/src/rbac.rs b/cedar-policy-validator/src/rbac.rs
index 8a41b1a5..5f7f3729 100644
--- a/cedar-policy-validator/src/rbac.rs
+++ b/cedar-policy-validator/src/rbac.rs
@@ -613,6 +613,8 @@ mod test {
                     attributes: None,
                     annotations: Annotations::new(),
                     loc: None,
+                    #[cfg(feature = "extended-schema")]
+                    defn_loc: None,
                 },
             )],
         );
@@ -747,6 +749,8 @@ mod test {
                     attributes: None,
                     annotations: Annotations::new(),
                     loc: None,
+                    #[cfg(feature = "extended-schema")]
+                    defn_loc: None,
                 },
             )],
         );
@@ -781,6 +785,8 @@ mod test {
                     attributes: None,
                     annotations: Annotations::new(),
                     loc: None,
+                    #[cfg(feature = "extended-schema")]
+                    defn_loc: None,
                 },
             )],
         );
@@ -982,6 +988,8 @@ mod test {
                     attributes: None,
                     annotations: Annotations::new(),
                     loc: None,
+                    #[cfg(feature = "extended-schema")]
+                    defn_loc: None,
                 },
             )],
         );
@@ -1011,6 +1019,8 @@ mod test {
                     attributes: None,
                     annotations: Annotations::new(),
                     loc: None,
+                    #[cfg(feature = "extended-schema")]
+                    defn_loc: None,
                 },
             )],
         );
@@ -1040,6 +1050,8 @@ mod test {
                     attributes: None,
                     annotations: Annotations::new(),
                     loc: None,
+                    #[cfg(feature = "extended-schema")]
+                    defn_loc: None,
                 },
             )],
         );
@@ -1126,6 +1138,8 @@ mod test {
                     attributes: None,
                     annotations: Annotations::new(),
                     loc: None,
+                    #[cfg(feature = "extended-schema")]
+                    defn_loc: None,
                 },
             )],
         )
@@ -1543,6 +1557,8 @@ mod test {
                         attributes: None,
                         annotations: Annotations::new(),
                         loc: None,
+                        #[cfg(feature = "extended-schema")]
+                        defn_loc: None,
                     },
                 ),
                 (
@@ -1556,6 +1572,8 @@ mod test {
                         attributes: None,
                         annotations: Annotations::new(),
                         loc: None,
+                        #[cfg(feature = "extended-schema")]
+                        defn_loc: None,
                     },
                 ),
                 (
@@ -1566,6 +1584,8 @@ mod test {
                         attributes: None,
                         annotations: Annotations::new(),
                         loc: None,
+                        #[cfg(feature = "extended-schema")]
+                        defn_loc: None,
                     },
                 ),
             ],
diff --git a/cedar-policy-validator/src/schema.rs b/cedar-policy-validator/src/schema.rs
index f6e3f340..ab4c8168 100644
--- a/cedar-policy-validator/src/schema.rs
+++ b/cedar-policy-validator/src/schema.rs
@@ -528,15 +528,22 @@ impl ValidatorSchema {
                 // error for any other undeclared entity types by
                 // `check_for_undeclared`.
                 let descendants = entity_children.remove(&name).unwrap_or_default();
+
                 match entity_type {
                     EntityTypeFragment::Enum(choices) => Ok((
                         name.clone(),
-                        ValidatorEntityType::new_enum(name, descendants, choices),
+                        ValidatorEntityType::new_enum(
+                            name.clone(),
+                            descendants,
+                            choices,
+                            name.loc().cloned(),
+                        ),
                     )),
                     EntityTypeFragment::Standard {
                         attributes,
                         parents: _,
                         tags,
+                        loc,
                     } => {
                         let (attributes, open_attributes) = {
                             let unresolved =
@@ -555,14 +562,16 @@ impl ValidatorSchema {
                             .transpose()?
                             .map(|unresolved| unresolved.resolve_common_type_refs(&common_types))
                             .transpose()?;
+
                         Ok((
-                            name.clone(),
+                            name.with_loc(loc.as_ref()),
                             ValidatorEntityType::new_standard(
                                 name,
                                 descendants,
                                 attributes,
                                 open_attributes,
                                 tags,
+                                loc,
                             ),
                         ))
                     }
@@ -602,6 +611,7 @@ impl ValidatorSchema {
                         context: Type::record_with_attributes(context, open_context_attributes),
                         attribute_types: action.attribute_types,
                         attributes: action.attributes,
+                        loc: action.loc,
                     },
                 ))
             })
@@ -3755,6 +3765,7 @@ mod test_rfc70 {
                 &miette::Report::new(e),
                 &ExpectedErrorMessageBuilder::error("definition of `NS::T` illegally shadows the existing definition of `T`")
                     .help("try renaming one of the definitions, or moving `T` to a different namespace")
+                    .exactly_one_underline("entity T { bar: String };")
                     .build(),
             );
         });
@@ -3772,6 +3783,7 @@ mod test_rfc70 {
                 &miette::Report::new(e),
                 &ExpectedErrorMessageBuilder::error("definition of `NS::T` illegally shadows the existing definition of `T`")
                     .help("try renaming one of the definitions, or moving `T` to a different namespace")
+                    .exactly_one_underline("entity T { bar: String };")
                     .build(),
             );
         });
@@ -3905,6 +3917,7 @@ mod test_rfc70 {
                 &miette::Report::new(e),
                 &ExpectedErrorMessageBuilder::error("definition of `NS::T` illegally shadows the existing definition of `T`")
                     .help("try renaming one of the definitions, or moving `T` to a different namespace")
+                    .exactly_one_underline("entity T in T { foo: String };")
                     .build(),
             );
         });
@@ -3960,13 +3973,16 @@ mod test_rfc70 {
                 action A;
             }
         ";
+        let assertion = ExpectedErrorMessageBuilder::error("definition of `NS::Action::\"A\"` illegally shadows the existing definition of `Action::\"A\"`")
+        .help("try renaming one of the actions, or moving `Action::\"A\"` to a different namespace");
+        #[cfg(feature = "extended-schema")]
+        let assertion = assertion.exactly_one_underline("A");
+
         assert_matches!(collect_warnings(ValidatorSchema::from_cedarschema_str(src, Extensions::all_available())), Err(e) => {
             expect_err(
                 src,
                 &miette::Report::new(e),
-                &ExpectedErrorMessageBuilder::error("definition of `NS::Action::\"A\"` illegally shadows the existing definition of `Action::\"A\"`")
-                    .help("try renaming one of the actions, or moving `Action::\"A\"` to a different namespace")
-                    .build(),
+                &assertion.build()
             );
         });
 
diff --git a/cedar-policy-validator/src/schema/action.rs b/cedar-policy-validator/src/schema/action.rs
index 253c89c3..7ddb6a07 100644
--- a/cedar-policy-validator/src/schema/action.rs
+++ b/cedar-policy-validator/src/schema/action.rs
@@ -18,6 +18,7 @@
 
 use cedar_policy_core::{
     ast::{self, EntityType, EntityUID, PartialValue},
+    parser::Loc,
     transitive_closure::TCNode,
 };
 use smol_str::SmolStr;
@@ -58,6 +59,8 @@ pub struct ValidatorActionId {
     /// `Entity` for this action. Could also be used for more precise
     /// typechecking by partial evaluation.
     pub(crate) attributes: BTreeMap<SmolStr, PartialValue>,
+    /// Source location - if available
+    pub(crate) loc: Option<Loc>,
 }
 
 impl ValidatorActionId {
@@ -73,6 +76,7 @@ impl ValidatorActionId {
         context: Type,
         attribute_types: Attributes,
         attributes: BTreeMap<SmolStr, PartialValue>,
+        loc: Option<Loc>,
     ) -> Self {
         Self {
             name,
@@ -84,6 +88,7 @@ impl ValidatorActionId {
             context,
             attribute_types,
             attributes,
+            loc,
         }
     }
 
@@ -92,6 +97,11 @@ impl ValidatorActionId {
         &self.name
     }
 
+    /// The source location if available
+    pub fn loc(&self) -> Option<&Loc> {
+        self.loc.as_ref()
+    }
+
     /// Iterator over the actions that are members of this action
     pub fn descendants(&self) -> impl Iterator<Item = &EntityUID> {
         self.descendants.iter()
@@ -287,6 +297,7 @@ mod test {
             context: Type::any_record(),
             attribute_types: Attributes::default(),
             attributes: BTreeMap::default(),
+            loc: None,
         }
     }
 
diff --git a/cedar-policy-validator/src/schema/entity_type.rs b/cedar-policy-validator/src/schema/entity_type.rs
index 69a2e1aa..9d007900 100644
--- a/cedar-policy-validator/src/schema/entity_type.rs
+++ b/cedar-policy-validator/src/schema/entity_type.rs
@@ -21,7 +21,7 @@ use serde::Serialize;
 use smol_str::SmolStr;
 use std::collections::HashSet;
 
-use cedar_policy_core::{ast::EntityType, transitive_closure::TCNode};
+use cedar_policy_core::{ast::EntityType, parser::Loc, transitive_closure::TCNode};
 
 use crate::types::{AttributeType, Attributes, OpenTag, Type};
 
@@ -46,6 +46,10 @@ pub struct ValidatorEntityType {
     ///
     /// For enumerated entities, this is always empty.
     pub(crate) attributes: Attributes,
+
+    /// Source location - if available
+    #[serde(skip)]
+    pub loc: Option<Loc>,
 }
 
 /// The kind of validator entity types.
@@ -86,6 +90,7 @@ impl ValidatorEntityType {
         attributes: Attributes,
         open_attributes: OpenTag,
         tags: Option<Type>,
+        loc: Option<Loc>,
     ) -> Self {
         Self {
             name,
@@ -95,6 +100,7 @@ impl ValidatorEntityType {
                 open_attributes,
                 tags,
             }),
+            loc,
         }
     }
 
@@ -106,12 +112,14 @@ impl ValidatorEntityType {
         name: EntityType,
         descendants: impl IntoIterator<Item = EntityType>,
         values: NonEmpty<SmolStr>,
+        loc: Option<Loc>,
     ) -> Self {
         Self {
             name,
             descendants: descendants.into_iter().collect(),
             attributes: Attributes::with_attributes([]),
             kind: ValidatorEntityTypeKind::Enum(values),
+            loc,
         }
     }
 
diff --git a/cedar-policy-validator/src/schema/namespace_def.rs b/cedar-policy-validator/src/schema/namespace_def.rs
index ccd5b568..159bebf0 100644
--- a/cedar-policy-validator/src/schema/namespace_def.rs
+++ b/cedar-policy-validator/src/schema/namespace_def.rs
@@ -28,6 +28,7 @@ use cedar_policy_core::{
     evaluator::RestrictedEvaluator,
     extensions::Extensions,
     fuzzy_match::fuzzy_search,
+    parser::Loc,
 };
 use itertools::Itertools;
 use nonempty::{nonempty, NonEmpty};
@@ -287,7 +288,7 @@ impl CommonTypeDefs<ConditionalName> {
     ) -> crate::err::Result<Self> {
         let mut defs = HashMap::new();
         for (id, schema_ty) in schema_file_type_def {
-            let name = RawName::new_from_unreserved(id.into()).qualify_with(schema_namespace); // the declaration name is always (unconditionally) prefixed by the current/active namespace
+            let name = RawName::new_from_unreserved(id.into(), None).qualify_with(schema_namespace); // the declaration name is always (unconditionally) prefixed by the current/active namespace
             match defs.entry(name) {
                 Entry::Vacant(ventry) => {
                     ventry
@@ -313,7 +314,7 @@ impl CommonTypeDefs<ConditionalName> {
     ) -> crate::err::Result<Self> {
         let mut defs = HashMap::with_capacity(input_type_defs.len());
         for (id, schema_ty) in input_type_defs {
-            let name = RawName::new_from_unreserved(id).qualify_with(schema_namespace); // the declaration name is always (unconditionally) prefixed by the current/active namespace
+            let name = RawName::new_from_unreserved(id, None).qualify_with(schema_namespace); // the declaration name is always (unconditionally) prefixed by the current/active namespace
             match defs.entry(name) {
                 Entry::Vacant(ventry) => {
                     ventry.insert(schema_ty);
@@ -340,7 +341,7 @@ impl CommonTypeDefs<ConditionalName> {
     ) -> Self {
         Self {
             defs: HashMap::from_iter([(
-                RawName::new_from_unreserved(id).qualify_with(schema_namespace),
+                RawName::new_from_unreserved(id, None).qualify_with(schema_namespace),
                 schema_ty,
             )]),
         }
@@ -402,7 +403,8 @@ impl EntityTypesDef<ConditionalName> {
         let mut defs: HashMap<EntityType, _> = HashMap::new();
         for (id, entity_type) in schema_files_types {
             let ety = internal_name_to_entity_type(
-                RawName::new_from_unreserved(id).qualify_with(schema_namespace), // the declaration name is always (unconditionally) prefixed by the current/active namespace
+                RawName::new_from_unreserved(id, entity_type.loc.clone())
+                    .qualify_with(schema_namespace), // the declaration name is always (unconditionally) prefixed by the current/active namespace
             )?;
             match defs.entry(ety) {
                 Entry::Vacant(ventry) => {
@@ -474,6 +476,8 @@ pub enum EntityTypeFragment<N> {
         /// resolved/inlined (e.g., because they are not defined in this schema
         /// fragment).
         tags: Option<json_schema::Type<N>>,
+        /// Source location - if available
+        loc: Option<Loc>,
     },
     Enum(NonEmpty<SmolStr>),
 }
@@ -497,22 +501,26 @@ impl EntityTypeFragment<ConditionalName> {
     ) -> Self {
         match schema_file_type.kind {
             EntityTypeKind::Enum { choices } => Self::Enum(choices),
-            EntityTypeKind::Standard(ty) => Self::Standard {
-                attributes: ty
-                    .shape
-                    .conditionally_qualify_type_references(schema_namespace),
-                parents: ty
-                    .member_of_types
-                    .into_iter()
-                    .map(|raw_name| {
-                        // Only entity, not common, here for now; see #1064
-                        raw_name.conditionally_qualify_with(schema_namespace, ReferenceType::Entity)
-                    })
-                    .collect(),
-                tags: ty
-                    .tags
-                    .map(|tags| tags.conditionally_qualify_type_references(schema_namespace)),
-            },
+            EntityTypeKind::Standard(ty) => {
+                Self::Standard {
+                    attributes: ty
+                        .shape
+                        .conditionally_qualify_type_references(schema_namespace),
+                    parents: ty
+                        .member_of_types
+                        .into_iter()
+                        .map(|raw_name| {
+                            // Only entity, not common, here for now; see #1064
+                            raw_name
+                                .conditionally_qualify_with(schema_namespace, ReferenceType::Entity)
+                        })
+                        .collect(),
+                    tags: ty
+                        .tags
+                        .map(|tags| tags.conditionally_qualify_type_references(schema_namespace)),
+                    loc: None,
+                }
+            }
         }
     }
 
@@ -532,6 +540,7 @@ impl EntityTypeFragment<ConditionalName> {
                 attributes,
                 parents,
                 tags,
+                loc,
             } => {
                 // Fully qualify typenames appearing in `attributes`
                 let fully_qual_attributes = attributes.fully_qualify_type_references(all_defs);
@@ -560,6 +569,7 @@ impl EntityTypeFragment<ConditionalName> {
                         attributes,
                         parents,
                         tags,
+                        loc,
                     }),
                     (Ok(_), Ok(_), Some(undeclared_parents)) => Err(TypeNotDefinedError {
                         undefined_types: undeclared_parents,
@@ -616,6 +626,25 @@ impl<N, A> ActionsDef<N, A> {
     }
 }
 
+#[cfg_attr(not(feature = "extended-schema"), allow(unused_variables))]
+fn create_action_entity_uid_default_type(
+    action_name: &SmolStr,
+    action_type: &json_schema::ActionType<RawName>,
+    schema_namespace: Option<&InternalName>,
+) -> json_schema::ActionEntityUID<InternalName> {
+    let action_id_str = action_name.clone();
+    #[cfg(feature = "extended-schema")]
+    let action_id_loc = action_type.defn_loc.clone();
+    #[cfg(feature = "extended-schema")]
+    // the declaration name is always (unconditionally) prefixed by the current/active namespace
+    return json_schema::ActionEntityUID::default_type_with_loc(action_id_str, action_id_loc)
+        .qualify_with(schema_namespace);
+
+    #[cfg(not(feature = "extended-schema"))]
+    // the declaration name is always (unconditionally) prefixed by the current/active namespace
+    json_schema::ActionEntityUID::default_type(action_id_str).qualify_with(schema_namespace)
+}
+
 impl ActionsDef<ConditionalName, ConditionalName> {
     /// Construct an [`ActionsDef<ConditionalName>`] by converting the structures used by the
     /// schema format to those used internally by the validator.
@@ -625,21 +654,22 @@ impl ActionsDef<ConditionalName, ConditionalName> {
         extensions: &Extensions<'_>,
     ) -> crate::err::Result<Self> {
         let mut actions = HashMap::new();
-        for (action_id_str, action_type) in schema_file_actions {
-            let action_uid = json_schema::ActionEntityUID::default_type(action_id_str.clone())
-                .qualify_with(schema_namespace); // the declaration name is always (unconditionally) prefixed by the current/active namespace
-            match actions.entry(action_uid.try_into()?) {
+        for (action_name, action_type) in schema_file_actions {
+            let action_uid =
+                create_action_entity_uid_default_type(&action_name, &action_type, schema_namespace);
+            match actions.entry(action_uid.clone().try_into()?) {
                 Entry::Vacant(ventry) => {
                     let frag = ActionFragment::from_raw_action(
                         ventry.key(),
-                        action_type,
+                        action_type.clone(),
                         schema_namespace,
                         extensions,
+                        action_type.loc.as_ref(),
                     )?;
                     ventry.insert(frag);
                 }
                 Entry::Occupied(_) => {
-                    return Err(DuplicateActionError(action_id_str).into());
+                    return Err(DuplicateActionError(action_name).into());
                 }
             }
         }
@@ -697,6 +727,8 @@ pub struct ActionFragment<N, A> {
     /// separately so that we can later extract these values to construct the
     /// actual `Entity` objects defined by the schema.
     pub(super) attributes: BTreeMap<SmolStr, PartialValue>,
+    /// Source location - if available
+    pub(super) loc: Option<Loc>,
 }
 
 impl ActionFragment<ConditionalName, ConditionalName> {
@@ -705,6 +737,7 @@ impl ActionFragment<ConditionalName, ConditionalName> {
         action_type: json_schema::ActionType<RawName>,
         schema_namespace: Option<&InternalName>,
         extensions: &Extensions<'_>,
+        loc: Option<&Loc>,
     ) -> crate::err::Result<Self> {
         let (principal_types, resource_types, context) = action_type
             .applies_to
@@ -747,6 +780,7 @@ impl ActionFragment<ConditionalName, ConditionalName> {
                 .collect(),
             attribute_types,
             attributes,
+            loc: loc.cloned(),
         })
     }
 
@@ -770,6 +804,7 @@ impl ActionFragment<ConditionalName, ConditionalName> {
                 .partition_nonempty()?,
             attribute_types: self.attribute_types,
             attributes: self.attributes,
+            loc: self.loc,
         })
     }
 
diff --git a/cedar-policy-validator/src/schema/raw_name.rs b/cedar-policy-validator/src/schema/raw_name.rs
index a304e505..c92dba85 100644
--- a/cedar-policy-validator/src/schema/raw_name.rs
+++ b/cedar-policy-validator/src/schema/raw_name.rs
@@ -34,13 +34,13 @@ pub struct RawName(InternalName);
 
 impl RawName {
     /// Create a new [`RawName`] from the given [`Id`]
-    pub fn new(id: Id) -> Self {
-        Self(InternalName::unqualified_name(id))
+    pub fn new(id: Id, loc: Option<Loc>) -> Self {
+        Self(InternalName::unqualified_name(id, loc))
     }
 
     /// Create a new [`RawName`] from the given [`UnreservedId`]
-    pub fn new_from_unreserved(id: UnreservedId) -> Self {
-        Self::new(id.into())
+    pub fn new_from_unreserved(id: UnreservedId, loc: Option<Loc>) -> Self {
+        Self::new(id.into(), loc)
     }
 
     /// Create a new [`RawName`] from the given [`InternalName`].
diff --git a/cedar-policy/Cargo.toml b/cedar-policy/Cargo.toml
index 488942ba..716d36ba 100644
--- a/cedar-policy/Cargo.toml
+++ b/cedar-policy/Cargo.toml
@@ -52,7 +52,7 @@ corpus-timing = []
 
 # Experimental features.
 # Enable all experimental features with `cargo build --features "experimental"`
-experimental = ["partial-eval", "permissive-validate", "partial-validate", "level-validate", "entity-manifest", "protobufs", "datetime", "tolerant-ast"]
+experimental = ["partial-eval", "permissive-validate", "partial-validate", "level-validate", "entity-manifest", "protobufs", "datetime", "tolerant-ast", "extended-schema"]
 entity-manifest = ["cedar-policy-validator/entity-manifest"]
 partial-eval = ["cedar-policy-core/partial-eval", "cedar-policy-validator/partial-eval"]
 permissive-validate = []
@@ -61,6 +61,7 @@ protobufs = ["dep:prost", "dep:prost-build"]
 level-validate = ["cedar-policy-validator/level-validate"]
 wasm = ["serde-wasm-bindgen", "tsify", "wasm-bindgen"]
 tolerant-ast = ["cedar-policy-core/tolerant-ast",  "cedar-policy-validator/tolerant-ast", "cedar-policy-formatter/tolerant-ast"]
+extended-schema = ["cedar-policy-validator/extended-schema"]
 
 [lib]
 # cdylib required for wasm
diff --git a/cedar-policy/src/api.rs b/cedar-policy/src/api.rs
index 8b8dc31a..f418b0f6 100644
--- a/cedar-policy/src/api.rs
+++ b/cedar-policy/src/api.rs
@@ -4562,6 +4562,7 @@ action CreateList in Create appliesTo {
         let principals = schema.principals().collect::<Vec<_>>();
         assert!(principals.len() > 1);
         assert!(principals.iter().all(|ety| **ety == user));
+        assert!(principals.iter().all(|ety| ety.0.loc().is_some()));
     }
 
     #[test]
@@ -4581,6 +4582,7 @@ action CreateList in Create appliesTo {
             "CoolList".parse().unwrap(),
         ]);
         assert_eq!(resources, expected);
+        assert!(resources.iter().all(|ety| ety.0.loc().is_some()));
     }
 
     #[test]
@@ -4594,6 +4596,7 @@ action CreateList in Create appliesTo {
             .cloned()
             .collect::<Vec<_>>();
         assert_eq!(got, vec!["User".parse().unwrap()]);
+        assert!(got.iter().all(|ety| ety.0.loc().is_some()));
         assert!(schema.principals_for_action(&delete_user).is_none());
     }
 
@@ -4610,12 +4613,14 @@ action CreateList in Create appliesTo {
             .cloned()
             .collect::<Vec<_>>();
         assert_eq!(got, vec!["List".parse().unwrap()]);
+        assert!(got.iter().all(|ety| ety.0.loc().is_some()));
         let got = schema
             .resources_for_action(&create_list)
             .unwrap()
             .cloned()
             .collect::<Vec<_>>();
         assert_eq!(got, vec!["Application".parse().unwrap()]);
+        assert!(got.iter().all(|ety| ety.0.loc().is_some()));
         let got = schema
             .resources_for_action(&get_list)
             .unwrap()
@@ -4625,6 +4630,7 @@ action CreateList in Create appliesTo {
             got,
             HashSet::from(["List".parse().unwrap(), "CoolList".parse().unwrap()])
         );
+        assert!(got.iter().all(|ety| ety.0.loc().is_some()));
         assert!(schema.principals_for_action(&delete_user).is_none());
     }
 
@@ -4637,6 +4643,7 @@ action CreateList in Create appliesTo {
             .unwrap()
             .cloned()
             .collect::<HashSet<_>>();
+        assert!(parents.iter().all(|ety| ety.0.loc().is_some()));
         let expected = HashSet::from(["Team".parse().unwrap(), "Application".parse().unwrap()]);
         assert_eq!(parents, expected);
         let parents = schema
@@ -4644,6 +4651,7 @@ action CreateList in Create appliesTo {
             .unwrap()
             .cloned()
             .collect::<HashSet<_>>();
+        assert!(parents.iter().all(|ety| ety.0.loc().is_some()));
         let expected = HashSet::from(["Application".parse().unwrap()]);
         assert_eq!(parents, expected);
         assert!(schema.ancestors(&"Foo".parse().unwrap()).is_none());
@@ -4652,6 +4660,7 @@ action CreateList in Create appliesTo {
             .unwrap()
             .cloned()
             .collect::<HashSet<_>>();
+        assert!(parents.iter().all(|ety| ety.0.loc().is_some()));
         let expected = HashSet::from([]);
         assert_eq!(parents, expected);
     }
@@ -4664,6 +4673,8 @@ action CreateList in Create appliesTo {
             .into_iter()
             .map(|ty| format!("Action::\"{ty}\"").parse().unwrap())
             .collect::<HashSet<EntityUid>>();
+        #[cfg(feature = "extended-schema")]
+        assert!(groups.iter().all(|ety| ety.0.loc().is_some()));
         assert_eq!(groups, expected);
     }
 
@@ -4689,6 +4700,8 @@ action CreateList in Create appliesTo {
         .map(|ty| format!("Action::\"{ty}\"").parse().unwrap())
         .collect::<HashSet<EntityUid>>();
         assert_eq!(actions, expected);
+        #[cfg(feature = "extended-schema")]
+        assert!(actions.iter().all(|ety| ety.0.loc().is_some()));
     }
 
     #[test]
diff --git a/cedar-policy/src/proto/validator.rs b/cedar-policy/src/proto/validator.rs
index 41facd97..648af29f 100644
--- a/cedar-policy/src/proto/validator.rs
+++ b/cedar-policy/src/proto/validator.rs
@@ -123,6 +123,7 @@ impl From<&models::ActionDecl> for cedar_policy_validator::ValidatorActionId {
             // translate into a `ValidatorActionId` with no action attributes
             types::Attributes::with_attributes([]),
             BTreeMap::new(),
+            None,
         )
     }
 }
@@ -166,13 +167,14 @@ impl From<&models::EntityDecl> for cedar_policy_validator::ValidatorEntityType {
                 model_to_attributes(&v.attributes),
                 types::OpenTag::default(),
                 v.tags.as_ref().map(types::Type::from),
+                None,
             ),
             Some(enum_choices) => {
                 // `enum_choices` is not empty, so `v` represents an enumerated entity type.
                 // enumerated entity types must have no attributes or tags.
                 assert_eq!(&v.attributes, &HashMap::new());
                 assert_eq!(&v.tags, &None);
-                Self::new_enum(name, descendants, enum_choices)
+                Self::new_enum(name.clone(), descendants, enum_choices, name.loc().cloned())
             }
         }
     }


Test patch:
diff --git a/cedar-policy-validator/src/cedar_schema/test.rs b/cedar-policy-validator/src/cedar_schema/test.rs
index bbb181da..f502c277 100644
--- a/cedar-policy-validator/src/cedar_schema/test.rs
+++ b/cedar-policy-validator/src/cedar_schema/test.rs
@@ -311,6 +311,8 @@ mod demo_tests {
             member_of: None,
             annotations: Annotations::new(),
             loc: None,
+            #[cfg(feature = "extended-schema")]
+            defn_loc: None,
         };
         let namespace =
             json_schema::NamespaceDefinition::new(empty(), once(("foo".to_smolstr(), action)));
@@ -426,6 +428,8 @@ namespace Baz {action "Foo" appliesTo {
                     member_of: None,
                     annotations: Annotations::new(),
                     loc: None,
+                    #[cfg(feature = "extended-schema")]
+                    defn_loc: None,
                 },
             )]),
         );
@@ -1479,6 +1483,7 @@ mod translator_tests {
                 &miette::Report::new(e),
                 &ExpectedErrorMessageBuilder::error("definition of `Demo::email_address` illegally shadows the existing definition of `email_address`")
                     .help("try renaming one of the definitions, or moving `email_address` to a different namespace")
+                    .exactly_one_underline("entity email_address {\n              where: String,\n            };")
                     .build(),
             );
         });