Description
JSON Schema Dynamic References in TypeSpec
This proposal adds support for JSON Schema dynamic references ($dynamicRef
and $dynamicAnchor
) to the @typespec/json-schema
package.
Background and Motivation
JSON Schema 2020-12 introduced $dynamicRef
and $dynamicAnchor
, which enable polymorphic schema reuse, recursive type specialization, and dynamic resolution of references at runtime. Unlike standard $ref
, which resolves statically and lexically, dynamic references resolve based on the evaluation path. This late-binding mechanism supports complex patterns like recursive polymorphism and context-sensitive schema composition.
The Problem Dynamic References Solve
Common scenarios where traditional $ref
is too rigid:
- A document structure where folders can contain other folders or typed files
- A financial hierarchy where accounts contain specialized sub-accounts
- A UI component tree where containers nest other specialized components
With standard $ref
, recursive structures always resolve to the original definition, so specialized behavior or constraints in extended types can't be properly enforced. Dynamic references enable the validator to select the right schema depending on where the evaluation started—ensuring proper validation at all nesting levels.
This unlocks:
- True recursive polymorphism — Properly validate recursive structures with specialization at any level
- Generic schema patterns — Create reusable patterns that adapt to their context
- More precise validation — Capture complex object relationships and inheritance
- Schema composition — Build complex schemas from simpler building blocks
- Runtime-aware validation — Context-sensitive resolution of references based on evaluation path
This capability is essential for accurately modeling many real-world domains in APIs, from document management systems to financial services, organization hierarchies to UI component libraries.
Proposal
Add new decorators to the TypeSpec JSON Schema library:
/**
* Marks the target schema location with a `$dynamicAnchor`, enabling it
* to be referenced with `$dynamicRef` during validation.
*
* Multiple schemas may declare the same dynamic anchor name; resolution
* is handled dynamically by the JSON Schema processor at runtime.
*
* @param name The name of the dynamic anchor
*/
extern dec dynamicAnchor(target: Model | Scalar | Enum | Union | Reflection.ModelProperty, name: valueof string);
/**
* Creates a JSON Schema dynamic reference that resolves against the nearest $dynamicAnchor
* in the evaluation path rather than using lexical scoping.
* This enables true polymorphic references in recursive schemas.
*
* The decorator does not validate that the anchor exists—resolution is
* deferred to runtime and handled by the JSON Schema validator.
*
* @param uri The URI reference, including the dynamic anchor fragment
*/
extern dec dynamicRef(target: Model | Scalar | Enum | Union | Reflection.ModelProperty, uri: valueof string);
These decorators map directly to JSON Schema 2020-12 keywords and defer all dynamic resolution to the schema consumer.
Examples
Polymorphic Tree Structure
@jsonSchema
namespace Trees {
// Base node definition with a dynamic anchor named "node"
// This anchor enables polymorphic resolution at runtime.
@dynamicAnchor("node")
model Node {
id: string;
metadata: Record<string, string>;
// Use of dynamicRef allows this reference to resolve not just to Node,
// but to any model that redefines the "node" anchor, such as FolderNode or FileNode.
@dynamicRef("#node")
children: Node[];
}
// Specialization: FileNode with its own constraints, reusing the same anchor
// At runtime, when a validator evaluates FileNode, it will resolve "#node"
// to FileNode itself, enabling recursive specialization.
@dynamicAnchor("node")
model FileNode extends Node {
content: string;
size: number;
// Note: We don't need to redefine children here as it's inherited,
// but the @dynamicRef will resolve to the nearest @dynamicAnchor
// in the evaluation path, respecting our specialized validation
}
// Another specialization with stricter constraints
@dynamicAnchor("node")
model FolderNode extends Node {
@minItems(1)
// Ensures folders must have at least one child
// The dynamicRef here will properly validate against any node type
// including FileNode, FolderNode and the base Node
children: Node[];
}
}
This example demonstrates a recursive tree structure where the children
property can contain any type of node (base Node
, FileNode
, or FolderNode
), and the validation correctly handles specialized node types at any level of nesting. Without dynamic references, this polymorphic behavior wouldn't be possible because standard $ref
would always point to the original Node
definition regardless of context.
Reusable Generic Schemas
@jsonSchema
namespace Collections {
// The base item definition with a shared anchor name
@dynamicAnchor("item")
model Item {
id: string;
}
// Generic collection model that uses a dynamicRef to the current item anchor
// This allows the collection to adapt to its context—each instantiation
// will resolve "#item" to the appropriate specialization.
model Collection<T extends Item> {
// Dynamic reference lets us use the most specific item type definition
@dynamicRef("#item")
items: T[];
count: number;
}
// Product specializes Item with additional properties
@dynamicAnchor("item")
model Product extends Item {
name: string;
price: number;
}
// ProductCatalog uses the Collection pattern with Product items
model ProductCatalog extends Collection<Product> {
category: string;
// Here the dynamic reference resolves to Product's anchor,
// ensuring proper validation of all product properties
}
// User also specializes Item with different properties
@dynamicAnchor("item")
model User extends Item {
username: string;
email: string;
}
// UserDirectory uses the same Collection pattern but with User items
model UserDirectory extends Collection<User> {
department: string;
// Here the dynamic reference resolves to User's anchor,
// providing proper validation for user properties instead
}
}
This example shows how dynamic references enable generic schema patterns where a base collection schema can be specialized for different item types while maintaining proper validation. The @dynamicRef
decorator allows the schema to adapt based on which specialized item type is being used, providing correct context-specific validation.
Component Composition
@jsonSchema
namespace UIComponents {
// All components share a dynamic anchor, allowing nested structures
@dynamicAnchor("component")
model Component {
id: string;
visible: boolean;
}
// Containers can contain any component—including themselves—thanks to dynamicRef
@dynamicAnchor("component")
model Container extends Component {
// Dynamic reference enables the container to hold any component type
@dynamicRef("#component")
children: Component[];
layout: "vertical" | "horizontal" | "grid";
}
// Button is a specialized component with its own validation
@dynamicAnchor("component")
model Button extends Component {
label: string;
action: string;
// Buttons are leaf components - they don't contain other components
}
// Form specializes Container with additional form-specific properties
@dynamicAnchor("component")
model Form extends Container {
@dynamicRef("#component")
children: Component[]; // Will validate correctly with any component type
submitAction: string;
// Teaching point: Even though Form extends Container which already has
// a children property, we redefine it here to be explicit about the design.
// The dynamicRef ensures proper validation based on the component hierarchy.
}
}
This demonstrates a UI component composition system where containers can hold any component type, including other containers, and the validation logic correctly applies to all nested components. The dynamic references ensure that specialized component types are properly validated no matter where they appear in the component tree.
Technical Implementation
Library State Keys
Add new state keys in lib.ts
:
export const $lib = createTypeSpecLibrary({
// ... existing code ...
state: {
// ... existing state ...
"JsonSchema.dynamicAnchor": {
description: "Contains data configured with @dynamicAnchor decorator"
},
"JsonSchema.dynamicRef": {
description: "Contains data configured with @dynamicRef decorator"
},
},
} as const);
Decorator Implementation
Add getters/setters in decorators.ts
:
export const [
/** Get dynamic anchor name set by `@dynamicAnchor` decorator */
getDynamicAnchor,
setDynamicAnchor,
/** {@inheritdoc DynamicAnchorDecorator} */
$dynamicAnchor,
] = createDataDecorator<DynamicAnchorDecorator, string>(
JsonSchemaStateKeys["JsonSchema.dynamicAnchor"]
);
export const [
/** Get dynamic reference URI set by `@dynamicRef` decorator */
getDynamicRef,
setDynamicRef,
/** {@inheritdoc DynamicRefDecorator} */
$dynamicRef,
] = createDataDecorator<DynamicRefDecorator, string>(
JsonSchemaStateKeys["JsonSchema.dynamicRef"]
);
Schema Generation
Update #applyConstraints
in json-schema-emitter.ts
:
#applyConstraints(
type: Scalar | Model | ModelProperty | Union | UnionVariant | Enum,
schema: ObjectBuilder<unknown>,
) {
// ... existing code ...
// Apply dynamic anchor
const dynamicAnchorName = getDynamicAnchor(this.emitter.getProgram(), type);
if (dynamicAnchorName !== undefined) {
schema.set("$dynamicAnchor", dynamicAnchorName);
}
// Apply dynamic reference
const dynamicRefUri = getDynamicRef(this.emitter.getProgram(), type);
if (dynamicRefUri !== undefined) {
schema.set("$dynamicRef", dynamicRefUri);
}
// ... remainder of existing code ...
}
Runtime Behavior and Semantics
- These decorators emit JSON Schema as specified in the 2020-12 draft.
- The TypeSpec compiler does not verify anchor resolution—this is deferred to the validator.
- Multiple
@dynamicAnchor("foo")
declarations are permitted and expected for polymorphism. $dynamicRef
will resolve at runtime, based on the closest matching anchor in the instance evaluation path.- No
$ref
is emitted when@dynamicRef
is used—this is a distinct mechanism.
Alternative Approaches Considered
1. Combined Decorator
Instead of separate @dynamicAnchor
and @dynamicRef
decorators, use a combined approach:
extern dec dynamicLink(
target: Model | Scalar | Enum | Union | Reflection.ModelProperty,
mode: "anchor" | "ref",
value: string
);
Pros:
- Single decorator to handle both cases
- Might be easier to implement
Cons:
- Less clear and intuitive API
- Doesn't match JSON Schema's distinct keywords
- Requires an extra parameter to distinguish modes
2. Using Existing Extension Decorator
The existing @extension
decorator could handle this without new decorators:
@extension("$dynamicAnchor", "node")
@extension("$dynamicRef", "#node")
Pros:
- No new decorators needed
- Already supported
Cons:
- Less discoverable
- No specific validation for URI formats
- Doesn't communicate intent as clearly
- More error-prone with direct string manipulation
Technical Considerations
1. URI Reference Validation
The implementation should validate that $dynamicRef
URIs are properly formatted, especially when they include fragments.
2. JSON Schema Version Compatibility
The $dynamicRef
and $dynamicAnchor
keywords were introduced in JSON Schema 2020-12. The implementation should ensure it's using this schema version or newer.
3. Schema Resolution
Special care must be taken to ensure that dynamic anchors and references are properly resolved during schema evaluation. This may require coordination with JSON Schema validators used in conjunction with TypeSpec.
4. Circular Reference Handling
Dynamic references can easily create circular references. The implementation should handle these appropriately without causing infinite recursion during schema generation.
Optional Enhancements
- Support for
@schemaId(...)
could improve cross-file anchor resolution. - A future
@dynamicRefTo(Foo)
decorator could help reduce string usage. - A linter rule could flag anchors that are never referenced, or vice versa, for better developer experience.
Real-World Use Cases
1. Document Management Systems
Document management systems commonly represent folder hierarchies where folders can contain other folders or documents. Dynamic references allow proper validation of this structure with type-specific validations at any nesting level.
// Document represents a leaf node in the hierarchy
model Document {
name: string;
content: string;
}
// Base item with common properties
@dynamicAnchor("item")
model Item {
name: string;
created: string;
modified: string;
}
// File is a specialized item with content
@dynamicAnchor("item")
model File extends Item {
size: number;
content: string;
// Teaching point: Files don't have child items,
// so they don't need the contents property
}
// Folder can contain other items (files or folders)
@dynamicAnchor("item")
model Folder extends Item {
// Dynamic reference ensures proper validation of each item type
@dynamicRef("#item")
contents: Item[];
// Note: Each item in contents will be validated against its most
// specific schema based on the evaluation path
}
// FileSystem represents the root of the hierarchy
model FileSystem {
// Root is always a folder, but it will use the dynamic anchor/ref system
@dynamicRef("#item")
root: Folder;
}
2. Financial Account Hierarchies
Financial systems often model account hierarchies where accounts can contain sub-accounts with specialized validation rules.
// Base account model with common properties and sub-accounts
@dynamicAnchor("account")
model Account {
id: string;
name: string;
balance: number;
// Can contain nested accounts of various types
@dynamicRef("#account")
subAccounts: Account[];
}
// SavingsAccount specializes Account but cannot have sub-accounts
@dynamicAnchor("account")
model SavingsAccount extends Account {
interestRate: number;
// Override with empty array and maxItems(0) to prevent sub-accounts
@maxItems(0)
subAccounts: never[]; // Cannot have sub-accounts
// Teaching point: We're using maxItems(0) and never[] together to
// both document and enforce that savings accounts can't have sub-accounts
}
// InvestmentAccount allows nested sub-accounts with specific risk profiles
@dynamicAnchor("account")
model InvestmentAccount extends Account {
riskLevel: "low" | "medium" | "high";
// Allows further account nesting with proper validation
@dynamicRef("#account")
subAccounts: Account[]; // Can have any type of sub-account
}
3. UI Component Libraries
UI design systems use component hierarchies where containers can hold other containers and basic components.
// Base component with common properties
@dynamicAnchor("component")
model Component {
id: string;
visible: boolean;
}
// Text component is a specialized leaf component
@dynamicAnchor("component")
model TextComponent extends Component {
text: string;
fontSize: number;
// Teaching point: Leaf components don't have children
}
// Container holds other components in a specific layout
@dynamicAnchor("component")
model ContainerComponent extends Component {
// Can contain any component type with proper validation
@dynamicRef("#component")
children: Component[];
layout: "row" | "column";
// Teaching point: The dynamicRef ensures that each child component
// will be validated against its most specific schema definition
}
// Form is a specialized container with submit behavior
@dynamicAnchor("component")
model FormComponent extends ContainerComponent {
onSubmit: string;
// Children array is inherited but explicitly redefined for clarity
@dynamicRef("#component")
children: Component[];
// The form can contain text components, other containers, etc.,
// and each will be validated correctly
}
4. Organization Structures
Organizations have hierarchical structures with different types of units at different levels.
// Base organizational unit that can contain other units
@dynamicAnchor("unit")
model OrganizationalUnit {
id: string;
name: string;
// Can contain nested units of any type
@dynamicRef("#unit")
children: OrganizationalUnit[];
}
// Department is a unit with budget and headcount
@dynamicAnchor("unit")
model Department extends OrganizationalUnit {
budget: number;
headCount: number;
// Inherits children from OrganizationalUnit
// The dynamicRef ensures proper validation of all child units
}
// Teams are leaf units that don't have children
@dynamicAnchor("unit")
model Team extends OrganizationalUnit {
teamLead: string;
// Teams don't have child units - explicitly override to enforce this
@maxItems(0)
children: never[];
// Teaching point: This is a pattern for terminating a hierarchy branch.
// The @maxItems(0) constraint ensures no children can be added to a Team.
}
Benefits
-
True Polymorphism: Enable proper validation of recursive structures with specialization at any level.
-
Generic Schema Patterns: Create reusable patterns that adapt to their context, promoting code reuse and consistency.
-
Precise Inheritance Modeling: Accurately model inheritance relationships and specialized validations in complex object hierarchies.
-
Flexible Composition: Build complex schemas from simpler building blocks while maintaining proper validation at all levels.
-
Accurate Domain Modeling: Represent real-world hierarchical relationships with proper type-specific validation rules.
-
Future-Proof Schemas: Align with the latest JSON Schema standards and capabilities.
Limitations
-
Schema Complexity: Dynamic references introduce a higher level of complexity in schema design and understanding.
-
Validator Support: Not all JSON Schema validators may fully support dynamic references yet.
-
Performance Considerations: Schema validation with dynamic references may be more computationally intensive.
-
Learning Curve: Developers need to understand the difference between lexical and dynamic scoping to use these features effectively.
-
Resolution Deferred: TypeSpec can't check if a referenced anchor actually exists – resolution is handled at runtime.
-
Correct Bundling Required: Poorly structured schema emission (e.g., bundling two anchors of same name incorrectly) could result in unexpected behavior.
Checklist
- Follow our Code of Conduct
- Read the docs.
- Check that there isn't already an issue that request the same feature to avoid creating a duplicate.