Architecture

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

TermDefinition
SchemaA JSON Schema (Draft-07) definition in definitions/
ViewA named definition consisting of a projection and an optional presentation template
ProjectionA JMESPath expression that extracts and reshapes data from a schema instance
TemplateA text structure that defines how projected data is presented to humans
RenderingThe SDK's responsibility: taking a projection result + template and producing output in a specific format (text, JSON, YAML, markdown)
Schema RegistryThe collection of JSON Schemas and their associated views that an SDK can resolve at runtime
Schema InstanceA data object that conforms to a schema, identified by its schema_type and schema_version fields

1.3 Design Principles

  1. Declarative over imperative: Views describe what to extract and how to structure it, not how to implement the extraction.
  2. Built on standards: Projections use JMESPath, a query language with a formal specification, compliance test suite, and implementations in 10+ languages.
  3. Schema-bound: Views are versioned with their schema. When a schema version is published, its views are published with it and become immutable.
  4. SDK-agnostic: Any SDK that can parse JSON and evaluate JMESPath expressions can consume views.
  5. Format-neutral: Views define projection and structure. The output format (JSON, YAML, text, markdown) is chosen at render time by the consumer.
  6. Domain-specific: Each schema defines views that are meaningful for its domain. Not all schemas need the same views.
  7. 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:

TechnologyWhat it solvesWhy not adopted directly
JMESPathField selection, renaming, transforms, array projectionsAdopted as the projection layer
JSONPath (RFC 9535)Field selection from JSONSelection only -- cannot create new objects, rename fields, or apply transforms
JSONataSelection + transforms + string concatenationMore powerful than needed; fewer language implementations than JMESPath; not a standards-body specification
GraphQLField selection with aliasesRequires a server runtime; cannot be used standalone against JSON data
JQJSON transformationCLI-oriented; Turing-complete (overkill); poor library support outside C
Mustache/HandlebarsText templatingSolves only the presentation layer; no projection capabilities. Considered for templates but adds a dependency for minimal gain over a simpler custom format
JSON-LD FramingReshaping graph data into treesRDF-only; no transforms; no text templating
JSON:API Sparse FieldsetsRequesting field subsetsRuntime query parameter, not a definition format; no transforms
JSON Forms UI SchemaForm layout over JSON SchemaClosest architectural parallel (separate presentation file), but designed for input forms, not output views
SHACL / ShExRDF shape validationRDF-only; validation focus, not projection
OpenAPIAPI schema definitionsNo concept of views or projections; different representations require separate schemas
JSON-eJSON template parameterizationDesigned for generating configs, not projecting data; verbose operator syntax

Why JMESPath

JMESPath was chosen for the projection layer because:

  1. Formal specification with ABNF grammar and a compliance test suite
  2. Multi-language implementations: Python (jmespath), JavaScript (jmespath), TypeScript (@metrichor/jmespath), Go (go-jmespath), Java, Rust, PHP, Lua, .NET -- all with full compliance
  3. Battle-tested: Used internally by AWS CLI (boto3), Ansible, and other major tools
  4. 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
  5. 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}
PropertyTypeRequiredDescription
descriptionstringYesPurpose and use case for this view
projectionstringYesJMESPath expression that extracts and reshapes the data
templatestring or objectNoPresentation 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:

ViewPurposeExpected token range
one-linerSingle-line identification of the instance~15-50 tokens
summaryKey fields overview for context setting~80-200 tokens
fullComplete data, all fieldsVaries 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

PatternExample
Direct field accessid, 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

FunctionDescriptionExample
length(subject)Array/string/object sizelength(events)
join(glue, array)Concatenate stringsjoin(', ', tags)
not_null(arg, ...)First non-null valuenot_null(summary.score, \0`)`
sort(array)Sort arraysort(scores)
sort_by(array, expr)Sort by expressionsort_by(events, &time_offset.start)
min(array) / max(array)Min/max valuemax(scores)
sum(array)Sum of numberssum(events[].duration)
avg(array)Average of numbersavg(scores)
contains(subject, search)Membership testcontains(tags, 'urgent')
to_string(arg)Convert to stringto_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 json and yaml formats: SDKs SHOULD exclude null values from the output
  • For text and markdown formats: 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

PropertyTypeRequiredDescription
titlestringYesTitle line with placeholder support
sectionslistYesOrdered list of sections to render

Section types

Each section has a label and exactly one content type:

Content typeDescriptionRendered example (text format)
valueSingle-value lineSource: email (text, asynchronous)
listOne 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:

StrategyDescriptionUse case
CDN (default)Fetched from CDN on demand, cached locallyProduction -- always up-to-date, new schemas without reinstall
Local directoryLoaded 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:

  1. The JSON Schema file: {schema_type}/{schema_version}.json
  2. 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:

  1. Reads data.schema_type and data.schema_version (both required)
  2. Resolves the JSON Schema from the registry
  3. Validates data against the JSON Schema
  4. Loads the corresponding views
  5. Returns a SchemaInstance that 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:

FormatRendering method
textInterpolate template with projected data
jsonSerialize projected data as JSON
yamlSerialize projected data as YAML
markdownInterpolate 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
    • value sections as: {label}: {interpolated value}
    • list sections as:
      {label}:
        - {item 1}
        - {item 2}
      
    • Skip sections where condition: exists and 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}
    • value sections as: - **{label}**: {interpolated value}
    • list sections as:
      ### {label}
      - {item 1}
      - {item 2}
      
    • Skip sections where condition: exists and the data is null

7.7 Error Handling

ScenarioBehavior
Missing schema_type or schema_version in dataRaise error: "Data must contain schema_type and schema_version fields"
Schema not found in registryRaise error: "Unknown schema: {schema_type}@{schema_version}"
Validation failsRaise error with JSON Schema validation details
Unknown view nameRaise error listing available views
Unknown formatRaise error listing supported formats
JMESPath evaluation returns null for a fieldExclude from JSON/YAML; skip template section if condition: exists
No template defined for text/markdown renderingFall 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:

  1. Every schema version MUST have a corresponding .views.json file
  2. Every .views.json MUST define one-liner, summary, and full views
  3. one-liner MUST have a simple template (string, not structured)
  4. full MUST have projection: "@"
  5. All projection values MUST be valid JMESPath expressions (parseable without errors)
  6. View names MUST be unique within a file
  7. View names MUST use kebab-case
  8. Every view (except full) MUST have a description

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:

  1. Create the JSON Schema: {schema-name}/{version}.json
  2. Create the documentation: {schema-name}/{version}.md
  3. 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:

  1. Copy the previous version's .views.json as a starting point
  2. Adapt JMESPath expressions to match any schema changes
  3. 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.