Schema Views Specification
Version: 1.0-draft Status: Draft Authors: Ontopix Platform Team Date: 2026-03-11
1. Overview
1.1 Purpose
Schema Views are declarative definitions that describe how to project and present data from a JSON Schema instance. They provide a standardized way to extract subsets of data and define human-readable templates for rendering.
Views solve a concrete problem: when multiple SDKs (Python, TypeScript, etc.) need to present schema data in consistent ways, the projection and presentation logic should not be duplicated in each SDK's codebase. Instead, it is defined once -- alongside the schema -- and consumed by all SDKs.
1.2 Terminology
| Term | Definition |
|---|---|
| Schema | A JSON Schema (Draft-07) definition in definitions/ |
| View | A named definition consisting of a projection and an optional presentation template |
| Projection | A JMESPath expression that extracts and reshapes data from a schema instance |
| Template | A text structure that defines how projected data is presented to humans |
| Rendering | The SDK's responsibility: taking a projection result + template and producing output in a specific format (text, JSON, YAML, markdown) |
| Schema Registry | The collection of JSON Schemas and their associated views that an SDK can resolve at runtime |
| Schema Instance | A data object that conforms to a schema, identified by its schema_type and schema_version fields |
1.3 Design Principles
- Declarative over imperative: Views describe what to extract and how to structure it, not how to implement the extraction.
- Built on standards: Projections use JMESPath, a query language with a formal specification, compliance test suite, and implementations in 10+ languages.
- Schema-bound: Views are versioned with their schema. When a schema version is published, its views are published with it and become immutable.
- SDK-agnostic: Any SDK that can parse JSON and evaluate JMESPath expressions can consume views.
- Format-neutral: Views define projection and structure. The output format (JSON, YAML, text, markdown) is chosen at render time by the consumer.
- Domain-specific: Each schema defines views that are meaningful for its domain. Not all schemas need the same views.
- Schema-agnostic SDKs: SDKs do not need compile-time knowledge of specific schemas. They discover schemas and views at runtime from a registry, enabling new schemas to be added without SDK code changes.
2. Prior Art and Standards Evaluation
Before designing this specification, the following standards and technologies were evaluated:
| Technology | What it solves | Why not adopted directly |
|---|---|---|
| JMESPath | Field selection, renaming, transforms, array projections | Adopted as the projection layer |
| JSONPath (RFC 9535) | Field selection from JSON | Selection only -- cannot create new objects, rename fields, or apply transforms |
| JSONata | Selection + transforms + string concatenation | More powerful than needed; fewer language implementations than JMESPath; not a standards-body specification |
| GraphQL | Field selection with aliases | Requires a server runtime; cannot be used standalone against JSON data |
| JQ | JSON transformation | CLI-oriented; Turing-complete (overkill); poor library support outside C |
| Mustache/Handlebars | Text templating | Solves only the presentation layer; no projection capabilities. Considered for templates but adds a dependency for minimal gain over a simpler custom format |
| JSON-LD Framing | Reshaping graph data into trees | RDF-only; no transforms; no text templating |
| JSON:API Sparse Fieldsets | Requesting field subsets | Runtime query parameter, not a definition format; no transforms |
| JSON Forms UI Schema | Form layout over JSON Schema | Closest architectural parallel (separate presentation file), but designed for input forms, not output views |
| SHACL / ShEx | RDF shape validation | RDF-only; validation focus, not projection |
| OpenAPI | API schema definitions | No concept of views or projections; different representations require separate schemas |
| JSON-e | JSON template parameterization | Designed for generating configs, not projecting data; verbose operator syntax |
Why JMESPath
JMESPath was chosen for the projection layer because:
- Formal specification with ABNF grammar and a compliance test suite
- Multi-language implementations: Python (
jmespath), JavaScript (jmespath), TypeScript (@metrichor/jmespath), Go (go-jmespath), Java, Rust, PHP, Lua, .NET -- all with full compliance - Battle-tested: Used internally by AWS CLI (boto3), Ansible, and other major tools
- Right level of power: Supports field selection, renaming (multi-select hash), array projection, and built-in functions (
length,join,sort,sum, etc.) without being Turing-complete - Transferable knowledge: Developers who learn JMESPath can apply it elsewhere (AWS CLI queries, Ansible filters, etc.)
A single JMESPath expression replaces what would otherwise require custom path, transform, fields, condition, and default properties in a bespoke format.
3. File Format and Location
3.1 File Location
View definitions live alongside their schema, following the naming convention:
definitions/{schema-name}/{version}.views.json
Examples:
definitions/customer-interaction/v1.0-beta1.views.json
definitions/audit-criteria/v1.0-beta1.views.json
definitions/audit-result/v1.0-beta1.views.json
3.2 File Structure
Views are defined in JSON (.views.json files). The file is an object where each top-level key is a view name.
Note: Examples in this specification use YAML-like notation for readability. Actual view files use JSON format.
view-name:
description: "Human-readable description of this view's purpose"
projection: "JMESPath expression"
template: "simple template" | {structured template}
| Property | Type | Required | Description |
|---|---|---|---|
description | string | Yes | Purpose and use case for this view |
projection | string | Yes | JMESPath expression that extracts and reshapes the data |
template | string or object | No | Presentation template for text/markdown rendering. Not required for full view or views intended only for machine formats |
3.3 View Naming Convention
- View names use
kebab-case - Names should be descriptive of the view's purpose
- Domain-specific views should use the
for-prefix when they represent a perspective (e.g.,for-audit,for-billing)
4. Mandatory Views
Every schema version MUST define the following views:
| View | Purpose | Expected token range |
|---|---|---|
one-liner | Single-line identification of the instance | ~15-50 tokens |
summary | Key fields overview for context setting | ~80-200 tokens |
full | Complete data, all fields | Varies by schema |
These mandatory views ensure that every schema has a baseline set of representations that SDKs and consumers can rely on.
Additional domain-specific views are encouraged and can be added freely.
5. Projections
5.1 JMESPath as the Projection Language
Each view's projection field is a JMESPath expression evaluated against the schema instance. The expression produces the projected data -- typically a JSON object with the desired fields.
JMESPath specification: https://jmespath.org/specification.html
5.2 Projection for full View
The full view uses the special projection value @, which returns the entire input (the identity projection in JMESPath):
full:
description: "Complete data with all fields"
projection: "@"
SDKs SHOULD exclude null values from the output when rendering the full view.
5.3 Projections with Multi-Select Hash
Most views use JMESPath multi-select hash to create a new object with selected and renamed fields:
projection: >
{
id: id,
channel: source.channel,
participants_count: length(participants),
participants: participants[].{id: id, role: role}
}
5.4 Common JMESPath Patterns
| Pattern | Example |
|---|---|
| Direct field access | id, source.channel |
| Renaming | {interaction_id: id} |
| Array length | {count: length(events)} |
| Array projection | {items: events[].{id: id, type: type}} |
| Null default | {score: not_null(summary.score, \0`)}` |
| Join | {tags: join(', ', tags)} |
| First/last | {first: events[0]} |
5.5 JMESPath Functions Reference
| Function | Description | Example |
|---|---|---|
length(subject) | Array/string/object size | length(events) |
join(glue, array) | Concatenate strings | join(', ', tags) |
not_null(arg, ...) | First non-null value | not_null(summary.score, \0`)` |
sort(array) | Sort array | sort(scores) |
sort_by(array, expr) | Sort by expression | sort_by(events, &time_offset.start) |
min(array) / max(array) | Min/max value | max(scores) |
sum(array) | Sum of numbers | sum(events[].duration) |
avg(array) | Average of numbers | avg(scores) |
contains(subject, search) | Membership test | contains(tags, 'urgent') |
to_string(arg) | Convert to string | to_string(score) |
Full specification: https://jmespath.org/specification.html#built-in-functions
5.6 Null Handling Convention
When the projection result contains null values:
- For
jsonandyamlformats: SDKs SHOULD exclude null values from the output - For
textandmarkdownformats: Template sections referencing null values are skipped (see Section 6.2)
6. Templates
Templates define how projected data is presented as structured text. They are used by SDKs when rendering to human-readable formats (text, markdown). For machine formats (json, yaml), SDKs serialize the projection result directly and the template is not used.
6.1 Simple Template
For views that produce a single line of text (typically one-liner):
template: "Interaction {id}: {participants_count} participants, {events_count} events"
Placeholders use {field_name} syntax, referencing keys from the projection result.
6.2 Structured Template
For views that produce multi-line structured output:
template:
title: "Customer Interaction: {id}"
sections:
- label: "Source"
value: "{channel} ({media}, {synchronicity})"
- label: "Participants"
list: "{participants[].role}: {participants[].id}"
- label: "Events"
value: "{events_count}"
- label: "Summary"
value: "{summary_text}"
condition: exists
Template properties
| Property | Type | Required | Description |
|---|---|---|---|
title | string | Yes | Title line with placeholder support |
sections | list | Yes | Ordered list of sections to render |
Section types
Each section has a label and exactly one content type:
| Content type | Description | Rendered example (text format) |
|---|---|---|
value | Single-value line | Source: email (text, asynchronous) |
list | One line per array element | - customer: cust-001- agent: agent-001 |
Conditional sections
Sections can include condition: exists to only render when the referenced data is non-null:
- label: "Summary"
value: "{summary_text}"
condition: exists
If the referenced field is null in the projection result, the entire section is skipped.
6.3 List Item Syntax
In list sections, the template iterates over an array field. Sub-field access uses [] notation:
list: "{participants[].role}: {participants[].id}"
This renders one line per array element, substituting sub-fields from each element.
6.4 No Template for full View
The full view uses projection: "@" and does not require a template. SDKs render it by serializing the entire instance. If a template is provided on a full view, it is ignored.
7. SDK Architecture
7.1 Schema-Agnostic Design
SDKs are generic runtimes that do not require compile-time knowledge of specific schemas. Instead, they discover schemas and views at runtime from a schema registry.
This means:
- Adding a new schema (or a new version of an existing schema) requires no SDK code changes
- The SDK is stable: its code does not evolve when schemas evolve
- Schemas and views are data, not code -- they are loaded from the registry at runtime
7.2 Schema Registry
The schema registry is the collection of JSON Schemas (.json) and view definitions (.views.json) that an SDK can resolve. The registry supports multiple loading strategies:
| Strategy | Description | Use case |
|---|---|---|
| CDN (default) | Fetched from CDN on demand, cached locally | Production -- always up-to-date, new schemas without reinstall |
| Local directory | Loaded from a filesystem path (ONTOPIX_SCHEMAS_DIR env var) | Development, testing |
SDKs MUST support the CDN strategy with local caching. SDKs MUST support local directory override via the ONTOPIX_SCHEMAS_DIR environment variable.
When using remote loading, SDKs SHOULD cache downloaded schemas and views locally to avoid repeated network requests.
Registry Resolution
Given schema_type and schema_version, the registry resolves:
- The JSON Schema file:
{schema_type}/{schema_version}.json - The views file:
{schema_type}/{schema_version}.views.json
7.3 Processing Pipeline
Raw Data (dict/JSON)
|
v
[Read schema_type + schema_version]
|
v
[Resolve from registry] --> JSON Schema + views.yaml
|
v
[Validate against JSON Schema]
|
v
SchemaInstance (validated data + loaded views)
|
v
instance.view("summary", format="markdown")
|
v
[JMESPath evaluation] <-- projection expression from views.yaml
|
v
Projected Data (JSON object)
|
+---> json format --> JSON serialization
+---> yaml format --> YAML serialization
+---> text format --> Template interpolation (plain text)
+---> markdown --> Template interpolation (markdown)
7.4 SDK Public API
Loading and Validation
All SDKs MUST provide a way to load and validate data:
SchemaInstance(data: dict/object) -> instance
The constructor:
- Reads
data.schema_typeanddata.schema_version(both required) - Resolves the JSON Schema from the registry
- Validates
dataagainst the JSON Schema - Loads the corresponding views
- Returns a
SchemaInstancethat provides access to the data and view rendering
If schema_type or schema_version is missing, an error is raised.
If the schema is not found in the registry, an error is raised.
If validation fails, an error is raised with details.
Data Access
SchemaInstance provides access to the underlying data:
instance.schema_type -> string
instance.schema_version -> string
instance.data -> dict/object (the validated raw data)
instance[key] or instance.get(key) -> field access
SDKs MAY provide dot-notation access for convenience (e.g., instance.get("source.channel")).
View Rendering
instance.view(name: string, format?: "text" | "json" | "yaml" | "markdown") -> string
name: The view name (e.g.,"summary","for-audit")format: Output format. Defaults to"text".- Returns: Rendered string.
- Raises/throws an error if the view name is not defined for the schema.
View Discovery
instance.views() -> list of {name, description}
Returns all available views for this schema instance.
Example Usage
Python:
from ontopix_schemas import SchemaInstance
# Load and validate any schema -- no import per schema type needed
instance = SchemaInstance({
"schema_type": "customer-interaction",
"schema_version": "v1.0-beta1",
"id": "int-12345",
"source": {"media": "text", "channel": "email", "synchronicity": "asynchronous"},
"time": {"start": "2026-01-28T10:30:00Z"},
"participants": [{"id": "cust-001", "role": "customer"}],
"events": [{
"id": "evt-001", "type": "message", "participant_id": "cust-001",
"time_offset": {"start": 0.0}, "content": {"text": "I need help"}
}]
})
# Access metadata
print(instance.schema_type) # "customer-interaction"
print(instance.schema_version) # "v1.0-beta1"
# Access data
print(instance["id"]) # "int-12345"
print(instance["source"]["channel"]) # "email"
# Render views
print(instance.view("one-liner"))
# Interaction int-12345: 1 participants, 1 events
print(instance.view("summary", format="markdown"))
# ## Customer Interaction: int-12345
# - **Source**: email (text, asynchronous)
# ...
print(instance.view("for-audit", format="json"))
# {"id": "int-12345", "channel": "email", ...}
# Discover available views
print(instance.views())
# [("one-liner", "Single-line identification..."), ("summary", "Key fields..."), ...]
TypeScript:
import { SchemaInstance } from '@ontopix/schemas';
const instance = new SchemaInstance({
schema_type: "customer-interaction",
schema_version: "v1.0-beta1",
id: "int-12345",
// ...
});
console.log(instance.view("summary"));
console.log(instance.view("for-audit", "json"));
console.log(instance.views());
7.5 Supported Formats
Every SDK MUST support these output formats:
| Format | Rendering method |
|---|---|
text | Interpolate template with projected data |
json | Serialize projected data as JSON |
yaml | Serialize projected data as YAML |
markdown | Interpolate template with markdown formatting |
7.6 Format-Specific Rendering Rules
json
Serialize the projection result as JSON. Exclude null values. Ignore the template.
- Non-full views: Pretty-printed JSON object
- Full view: Complete instance as JSON (excluding nulls)
yaml
Serialize the projection result as YAML. Exclude null values. Ignore the template.
text
Render using the template:
- Simple template: Interpolate placeholders and output the string.
- Structured template:
- Title as a plain line
valuesections as:{label}: {interpolated value}listsections as:{label}: - {item 1} - {item 2}- Skip sections where
condition: existsand the data is null
markdown
Render using the template with markdown formatting:
- Simple template: Interpolate placeholders. SDK may add bold formatting at its discretion.
- Structured template:
- Title as:
## {interpolated title} valuesections as:- **{label}**: {interpolated value}listsections as:### {label} - {item 1} - {item 2}- Skip sections where
condition: existsand the data is null
- Title as:
7.7 Error Handling
| Scenario | Behavior |
|---|---|
Missing schema_type or schema_version in data | Raise error: "Data must contain schema_type and schema_version fields" |
| Schema not found in registry | Raise error: "Unknown schema: {schema_type}@{schema_version}" |
| Validation fails | Raise error with JSON Schema validation details |
| Unknown view name | Raise error listing available views |
| Unknown format | Raise error listing supported formats |
| JMESPath evaluation returns null for a field | Exclude from JSON/YAML; skip template section if condition: exists |
| No template defined for text/markdown rendering | Fall back to JSON serialization of the projection |
7.8 Optional: Typed Wrappers
SDKs MAY additionally provide typed wrappers for specific schemas to offer compile-time type safety and IDE autocompletion. These are convenience layers on top of the generic SchemaInstance:
# Optional typed wrapper (Python with Pydantic)
from ontopix_schemas.typed import CustomerInteraction
interaction = CustomerInteraction(**data) # Pydantic validation + type hints
interaction.source.channel # IDE autocompletion
interaction.view("summary") # Same view() API
// Optional typed wrapper (TypeScript)
import { CustomerInteraction } from '@ontopix/schemas/typed';
const interaction: CustomerInteraction = data;
interaction.source.channel; // TypeScript type checking
Typed wrappers are an optional add-on, not the primary API. The primary API is always SchemaInstance.
8. Validation Rules
8.1 View File Validation
The following rules MUST be enforced by CI:
- Every schema version MUST have a corresponding
.views.jsonfile - Every
.views.jsonMUST defineone-liner,summary, andfullviews one-linerMUST have a simple template (string, not structured)fullMUST haveprojection: "@"- All
projectionvalues MUST be valid JMESPath expressions (parseable without errors) - View names MUST be unique within a file
- View names MUST use
kebab-case - Every view (except
full) MUST have adescription
8.2 Projection Validation
JMESPath expressions can be syntactically validated at CI time (parse without errors). Semantic validation (do the referenced fields exist in the schema?) is recommended but optional, as JMESPath gracefully returns null for missing fields at runtime.
9. Lifecycle
9.1 Creation
When creating a new schema version:
- Create the JSON Schema:
{schema-name}/{version}.json - Create the documentation:
{schema-name}/{version}.md - Create the views:
{schema-name}/{version}.views.json
No SDK changes are needed. The new schema and views will be picked up by the registry automatically.
When evolving from an existing version:
- Copy the previous version's
.views.jsonas a starting point - Adapt JMESPath expressions to match any schema changes
- Add, remove, or modify views as needed
9.2 Immutability
Views follow the same immutability rules as schemas:
- Once a schema version is published (deployed to production), its views file is immutable
- To change views, create a new schema version with updated views
- This ensures that consumers always get consistent behavior for a given schema version
9.3 Backward Compatibility
Since views are published with each schema version, there are no cross-version compatibility concerns. Each schema version carries its own complete set of views.
When creating a new schema version:
- SHOULD maintain the same mandatory view names (
one-liner,summary,full) - SHOULD maintain domain-specific view names when the use case still applies
- MAY add new domain-specific views
- MAY remove domain-specific views that are no longer relevant (with documentation)
10. Complete Examples
10.1 customer-interaction/v1.0-beta1.views.json
one-liner:
description: "Single-line identification of a customer interaction"
projection: >
{
id: id,
participants_count: length(participants),
events_count: length(events)
}
template: "Interaction {id}: {participants_count} participants, {events_count} events"
summary:
description: "Key fields for context setting and human review"
projection: >
{
id: id,
created_at: time.start,
channel: source.channel,
media: source.media,
synchronicity: source.synchronicity,
participants: participants[].{id: id, role: role, name: name},
events_count: length(events),
summary_text: summary.text
}
template:
title: "Customer Interaction: {id}"
sections:
- label: "Date"
value: "{created_at}"
- label: "Source"
value: "{channel} ({media}, {synchronicity})"
- label: "Participants"
list: "{participants[].role}: {participants[].id}"
- label: "Events"
value: "{events_count}"
- label: "Summary"
value: "{summary_text}"
condition: exists
full:
description: "Complete interaction data with all fields"
projection: "@"
for-audit:
description: "Projection optimized for quality auditing workflows"
projection: >
{
id: id,
channel: source.channel,
participants: participants[].{id: id, role: role},
events: events[].{
id: id,
type: type,
participant_id: participant_id,
text: content.text,
offset: time_offset.start
}
}
template:
title: "Interaction {id} (audit view)"
sections:
- label: "Channel"
value: "{channel}"
- label: "Participants"
list: "{participants[].id} ({participants[].role})"
- label: "Conversation"
list: "[{events[].offset}s] {events[].participant_id}: {events[].text}"
10.2 audit-criteria/v1.0-beta1.views.json
one-liner:
description: "Single-line identification of an audit policy"
projection: >
{
id: id,
name: name,
criteria_count: length(criteria)
}
template: "Policy {id}: {name} ({criteria_count} criteria)"
summary:
description: "Policy overview with targets and groups"
projection: >
{
id: id,
name: name,
description: description,
criteria_count: length(criteria),
targets: targets[].{schema: schema},
groups: groups[].{id: id, name: name, criteria_count: length(criteria_ids)}
}
template:
title: "Audit Criteria: {name}"
sections:
- label: "ID"
value: "{id}"
- label: "Description"
value: "{description}"
- label: "Criteria count"
value: "{criteria_count}"
- label: "Targets"
list: "{targets[].schema}"
condition: exists
- label: "Groups"
list: "{groups[].name}"
condition: exists
full:
description: "Complete audit criteria with all indicators and configuration"
projection: "@"
10.3 audit-result/v1.0-beta1.views.json
one-liner:
description: "Single-line audit result summary"
projection: >
{
id: id,
overall_score: not_null(summary.overall_score, `0`),
criteria_evaluated: not_null(summary.criteria_evaluated, `0`)
}
template: "Audit {id}: Score {overall_score} ({criteria_evaluated} criteria)"
summary:
description: "Audit result overview with scores and references"
projection: >
{
id: id,
timestamp: timestamp,
overall_score: not_null(summary.overall_score, `0`),
criteria_evaluated: not_null(summary.criteria_evaluated, `0`),
criteria_disabled: summary.criteria_disabled,
evaluated_objects: evaluated_objects[].{schema: schema, object_id: object_id},
policy_schema: audit_criteria.schema,
policy_object_id: audit_criteria.object_id
}
template:
title: "Audit Result: {id}"
sections:
- label: "Date"
value: "{timestamp}"
- label: "Overall Score"
value: "{overall_score}"
- label: "Criteria Evaluated"
value: "{criteria_evaluated}"
- label: "Criteria Disabled"
value: "{criteria_disabled}"
condition: exists
- label: "Evaluated Objects"
list: "{evaluated_objects[].schema}: {evaluated_objects[].object_id}"
condition: exists
- label: "Policy"
value: "{policy_schema} ({policy_object_id})"
condition: exists
full:
description: "Complete audit result with all criteria evaluations"
projection: "@"
for-report:
description: "Projection optimized for compliance reporting"
projection: >
{
id: id,
timestamp: timestamp,
overall_score: not_null(summary.overall_score, `0`),
criteria_results: criteria_results[].{
criterion_id: criterion_id,
name: name,
score: score,
indicator_count: length(indicator_results)
}
}
template:
title: "Audit Report: {id}"
sections:
- label: "Date"
value: "{timestamp}"
- label: "Overall Score"
value: "{overall_score}"
- label: "Criteria Results"
list: "{criteria_results[].name}: {criteria_results[].score} ({criteria_results[].indicator_count} indicators)"
11. Future Considerations
The following items are explicitly out of scope for v1.0 but may be addressed in future versions:
- Computed fields: Fields derived from formulas (e.g.,
duration = time.end - time.start). JMESPath's expression language may already support some of these. - Cross-schema views: Views that combine data from multiple related schema instances.
- Localization: Templates in multiple languages.
- View inheritance: A view extending another view with additional fields/sections.
- Custom JMESPath functions: SDK-registered functions available in projection expressions.
- Schema version ranges: Registry support for resolving "latest beta" or "any v1.x" instead of exact versions.