Skip to main content

Document Tree File Formats

This document provides complete specifications for all file formats used in VFS (Virtual File System) mode, where Morphir IR distributions are stored as a directory tree with individual files for each definition.

Serialization profile​

A document tree maps logical manifest, module, NAME.type, and NAME.value documents to one homogeneous physical serialization profile:

Logical documentJSON profileYAML profile
manifestmanifest.jsonmanifest.yaml
modulemodule.jsonmodule.yaml
NAME.typeNAME.type.jsonNAME.type.yaml
NAME.valueNAME.value.jsonNAME.value.yaml

The extension is not part of a logical identity. A generated tree MUST use one profile for every file. If discovery finds both manifest.json and manifest.yaml, it MUST report ambiguity and MUST NOT select one implicitly. The structures documented below apply to both profiles; JSON examples use the JSON profile, and their YAML equivalents use the YAML profile.

An Amazon Ion tree (manifest.ion) is an unreleased draft and is not part of this specification. It uses these logical paths, but its files hold annotated Ion elements instead of the structures below. See the Ion draft.

A v3 distribution uses this layout with classic payloads from IR 3.1.0; the v3 document tree page specifies it.

Logical paths​

A document tree is addressed by logical paths, and a logical path carries no extension:

manifest
pkg/<package path>/<module path>/module
pkg/<package path>/<module path>/<stem>.type
pkg/<package path>/<module path>/<stem>.value
deps/<package path>/@<version>/<module path>/module
deps/<package path>/@<version>/<module path>/<stem>.type
deps/<package path>/@<version>/<module path>/<stem>.value

<package path> is the escaped package name and <module path> is the escaped module name, one escaped stem per segment (see Naming). .type and .value are part of the logical name, not file extensions. Under deps/, the segment beginning with @ ends the package path and carries the package version; it is a bare @ while the v4 model carries no version (see Dependencies).

The profile decides the extension at the physical boundary and nowhere else: .json for the JSON profile, .yaml for the YAML profile. Going the other way, a physical name with .json, .yaml, or .yml maps back to its logical path; a file with any other extension is not part of the tree and is ignored. Because the extension is chosen at that boundary, the same logical tree renders into either profile without renaming anything.

mode is a concept of the Morphir Compatibility Kit, where a file fence may be marked mode=read so the kit driver checks only the read direction. It is not a concept of a document tree: no tree file carries a mode, and no reader looks for one.

Overview​

In VFS mode, a Morphir IR distribution is organized as:

.morphir-dist/
├── manifest.yaml # Distribution metadata
└── pkg/
└── package-name/
└── module-path/
├── module.yaml # Module manifest
├── type-name.type.yaml # Type definitions
└── value-name.value.yaml # Value definitions

The corresponding JSON tree replaces each .yaml extension with .json.

File Types​

1. Distribution Manifest (manifest.json)​

Location: .morphir-dist/manifest.json
Purpose: Distribution-level metadata and configuration

Required Fields:

  • formatVersion: IR format version
  • distribution: Distribution type ("Library", "Specs", or "Application")
  • package: Package name (canonical path format, e.g., "my-org/my-project")
  • pathBudget: Path budget in characters from the distribution root (decision 0012; 4000 default profile, 200 portable)

Optional Fields:

  • version: Package version (semantic version string)
  • created: Creation timestamp (ISO 8601 format)
  • layout: Distribution layout ("VfsMode" or "Classic", defaults to "VfsMode")
  • entryPoints: Entry points map (required for Application distributions)
  • dependencies: Array of package names, one per package that lives under deps/ (see Dependencies)

Schema: See morphir-ir-v4-document-tree-files.yaml → DistributionManifestFile

Example (Library):

{
"formatVersion": 4,
"distribution": "Library",
"package": "my-org/my-project",
"pathBudget": 4000,
"version": "1.2.0",
"created": "2026-01-15T12:00:00Z",
"layout": "VfsMode"
}

Example (Specs):

{
"formatVersion": 4,
"distribution": "Specs",
"package": "morphir/SDK",
"pathBudget": 4000,
"version": "3.0.0",
"created": "2026-01-15T12:00:00Z",
"layout": "VfsMode"
}

Example (Application):

{
"formatVersion": 4,
"distribution": "Application",
"package": "my-org/my-cli",
"pathBudget": 4000,
"version": "2.0.0",
"created": "2026-01-15T12:00:00Z",
"layout": "VfsMode",
"entryPoints": {
"startup": {
"target": "my-org/my-cli:main#run",
"kind": "main",
"doc": "Primary application entry point"
},
"build": {
"target": "my-org/my-cli:commands#build",
"kind": "command",
"doc": "Build the project"
}
}
}

2. Module Manifest (module.json)​

Location: .morphir-dist/pkg/package-name/module-path/module.json
Purpose: Module metadata and optional inline definitions

Required Fields:

  • formatVersion: IR format version
  • path or module: Module path (canonical format, e.g., "my-org/domain")

Note on path vs module fields:

Both path and module fields are equivalent and accepted for backwards compatibility. The path field is preferred for new files. When reading module.json files, tools should accept either field name.

Optional Fields:

  • access: Module visibility, "Public" or "Private". Placed after path. Defaults to "Public" when absent, and a canonical writer emits it only when the module is Private. A package definition's modules are access-controlled, so without this member a private module could not round-trip through a tree.
  • doc: Module-level documentation (string or array of strings)
  • types: Either array of type names (manifest style) or object with inline definitions (inline style)
  • values: Either array of value names (manifest style) or object with inline definitions (inline style)
  • fileNames: Canonical name to truncated file stem, for names whose stem was truncated for the path budget (decision 0012); every key must also appear in types or values

Encoding Styles:

Manifest Style (Granular)​

Lists type/value names; definitions in separate files:

{
"formatVersion": 4,
"path": "my-org/domain",
"doc": "Domain model for main application",
"types": ["user", "user-ID", "order"],
"values": ["get-user-by-email", "create-order", "validate-user"]
}

Example with a truncated stem:

{
"formatVersion": 4,
"path": "domain",
"types": ["customer-relationship-management-record"],
"values": [],
"fileNames": {
"customer-relationship-management-record": "customer-relati__44a101f8"
}
}

Example with legacy module field (equivalent to path):

{
"formatVersion": 4,
"module": "my-org/domain",
"doc": "Domain model for main application",
"types": ["user", "order"],
"values": ["create-order"]
}

Inline Style (Hybrid)​

Contains definitions directly:

{
"formatVersion": 4,
"path": "my-org/domain",
"doc": "Domain model for main application",
"types": {
"user": {
"access": "Public",
"doc": "Represents a user",
"TypeAliasDefinition": {
"typeParams": [],
"typeExp": {
"Record": {
"fields": {
"email": "morphir/SDK:string#string"
}
}
}
}
}
},
"values": {
"create-user": {
"access": "Public",
"ExpressionBody": {
"inputTypes": {},
"outputType": "my-org/domain:types#user",
"body": { "Literal": { "attributes": {}, "literal": { "StringLiteral": "..." } } }
}
}
}
}

Schema: See morphir-ir-v4-document-tree-files.yaml → ModuleManifestFile

3. Type Definition File (*.type.json)​

Location: .morphir-dist/pkg/package-name/module-path/type-name.type.json
Purpose: Individual type definition or specification

Required Fields:

  • formatVersion: IR format version
  • name: Type name (canonical format, must match filename without .type.json suffix)
  • Exactly one of:
    • def: Type definition (implementation) - contains TypeAliasDefinition, CustomTypeDefinition, or IncompleteTypeDefinition
    • spec: Type specification (interface) - contains TypeAliasSpecification, OpaqueTypeSpecification, CustomTypeSpecification, or DerivedTypeSpecification

Optional Fields:

  • doc: Documentation (string or array of strings), nested inside def or spec (decision 0010)

File Naming:

  • Use the escaped stem, escape(name), not the canonical name
  • Suffix: .type.json
  • Example: user.type.json, user-_id.type.json, order-line-item.type.json

Schema: See morphir-ir-v4-document-tree-files.yaml → TypeDefinitionFile

Example (Definition):

{
"formatVersion": 4,
"name": "user",
"def": {
"access": "Public",
"doc": "Represents a user in the system",
"TypeAliasDefinition": {
"typeParams": [],
"typeExp": {
"Record": {
"fields": {
"user-id": "my-org/domain:types#user-ID",
"email": "morphir/SDK:string#string",
"created-at": "my-org/SDK:local-date-time#local-date-time"
}
}
}
}
}
}

Note on Design Document Examples:

Some examples in the design documents (docs/design/draft/ir/distributions.md) may be simplified and omit the access field for brevity. In actual VFS files that are part of a PackageDefinition, the access field is required in def objects. The examples in this specification document show the complete, valid format.

Example (Specification):

{
"formatVersion": 4,
"name": "int",
"spec": {
"doc": "Arbitrary precision integer",
"OpaqueTypeSpecification": {}
}
}

Example (Custom Type Definition):

{
"formatVersion": 4,
"name": "order-status",
"def": {
"access": "Public",
"CustomTypeDefinition": {
"typeParams": [],
"access": "Public",
"value": {
"constructors": {
"pending": [],
"processing": [],
"shipped": [],
"delivered": [],
"cancelled": []
}
}
}
}
}

4. Value Definition File (*.value.json)​

Location: .morphir-dist/pkg/package-name/module-path/value-name.value.json
Purpose: Individual value definition or specification

Required Fields:

  • formatVersion: IR format version
  • name: Value name (canonical format, must match filename without .value.json suffix)
  • Exactly one of:
    • def: Value definition (implementation) - contains wrapper object with ExpressionBody, NativeBody, ExternalBody, or IncompleteBody
    • spec: Value specification (interface) - contains output (type) and, when non-empty, inputs (object)

Optional Fields:

  • doc: Documentation (string or array of strings), nested inside def or spec (decision 0010)

File Naming:

  • Use the escaped stem, escape(name), not the canonical name
  • Suffix: .value.json
  • Example: get-user-by-email.value.json, create-order.value.json, validate-email.value.json

Schema: See morphir-ir-v4-document-tree-files.yaml → ValueDefinitionFile

Example (Definition with ExpressionBody):

{
"formatVersion": 4,
"name": "get-user-by-email",
"def": {
"access": "Public",
"doc": "Retrieve a user by email address",
"ExpressionBody": {
"inputTypes": {
"email": "morphir/SDK:string#string",
"users": { "Reference": ["morphir/SDK:list#list", "my-org/domain:types#user"] }
},
"outputType": { "Reference": ["morphir/SDK:maybe#maybe", "my-org/domain:types#user"] },
"body": {
"Apply": {
"attributes": {},
"function": {
"Reference": {
"attributes": {},
"fqname": "morphir/SDK:list#find",
"args": []
}
},
"argument": {
"Variable": {
"attributes": {},
"name": "email"
}
}
}
}
}
}
}

Note on Type Reference Formats:

Type references in inputTypes, outputType, and typeExp fields are written as:

  • Bare FQName string for a reference with no type arguments: "morphir/SDK:string#string"
  • Reference wrapper for a parameterized reference: { "Reference": ["morphir/SDK:list#list", "my-org/domain:types#user"] }, where the first element is the type constructor and the rest are its arguments

A bare array is a tuple, not a reference. ["morphir/SDK:basics#int", "morphir/SDK:string#string"] is the pair (Int, String). That is what keeps the two forms unambiguous: a tuple of two plain types and a reference with one argument would otherwise have the same shape, so a parameterized reference always carries the wrapper.

Example (Definition with NativeBody):

{
"formatVersion": 4,
"name": "add",
"def": {
"access": "Public",
"NativeBody": {
"inputTypes": {
"a": "morphir/SDK:basics#int",
"b": "morphir/SDK:basics#int"
},
"outputType": "morphir/SDK:basics#int",
"nativeInfo": {
"hint": { "Arithmetic": {} }
}
}
}
}

Example (Specification):

{
"formatVersion": 4,
"name": "validate-email",
"spec": {
"doc": [
"Validate an email address format.",
"Returns true if the email is valid, false otherwise."
],
"inputs": {
"email": "morphir/SDK:string#string"
},
"output": "morphir/SDK:basics#bool"
}
}

Directory Structure​

Standard Layout​

.morphir-dist/
├── manifest.json # Distribution metadata
└── pkg/
└── my-org/
└── my-project/ # Package directory
├── domain/ # Module directory
│ ├── module.json # Module manifest
│ ├── user.type.json # Type definition
│ ├── user-_id.type.json # Type definition
│ ├── order.type.json # Type definition
│ ├── get-user.value.json # Value definition
│ └── create-order.value.json
└── api/ # Another module
├── module.json
├── request.type.json
└── handle-request.value.json

Nested Modules​

Modules can be nested by creating subdirectories:

.morphir-dist/
└── pkg/
└── my-org/
└── my-project/
└── domain/
├── module.json # domain module
├── user.type.json
└── orders/ # domain/orders submodule
├── module.json # domain/orders module
├── order.type.json
└── shipping/ # domain/orders/shipping submodule
├── module.json # domain/orders/shipping module
└── address.type.json

Dependencies​

A distribution's dependencies live under deps/. Each dependency's package path nests as directories, exactly as under pkg/, and is followed by one version segment that begins with @. Below that segment the layout is the one pkg/ uses: a module file per module and one file per type or value. The version segment is a bare @ while the v4 model carries no package version, and it carries the version (@3.0.0) once it does (decision 0015):

.morphir-dist/
├── manifest.yaml
├── pkg/
│ └── my-org/
│ └── my-project/
│ └── domain/
│ ├── module.yaml
│ └── user.type.yaml
└── deps/
└── morphir/
└── _sdk/
└── @/ # version segment: bare @ until the model carries a version
└── basics/
├── module.yaml
└── int.type.yaml

The distribution manifest MUST list every dependency package under dependencies, so that discovery does not have to walk deps/ blindly:

formatVersion: 4
distribution: Library
package: my-org/my-project
pathBudget: 4000
dependencies: [morphir/SDK]

Library and Specs dependencies are package specifications, so their node files carry spec. Application dependencies are package definitions, so their node files carry def.

The version segment exists so that a reader never has to guess where a package path ends: package paths and module paths are both multi-segment, and without the segment a tree holding packages a and a/b could not tell module b/c of a from module c of a/b. pkg/ carries no segment because a tree holds exactly one own package and the manifest names it. While the model carries no package version the segment is a bare @, a tree holds exactly one revision of each dependency, and a reader MUST report a deps/ directory whose segment carries a version. When package versioning lands, the version fills the segment and no other path changes.

Module order and annotations​

A directory carries no order. A tree is therefore read with its modules in sorted logical-path order, and a distribution written to a tree and read back has its modules in that order. Order is not semantic, but it is determined, so two readers of the same tree agree.

A module specification's annotations have no place in a tree file in 4.0.0. Reading a module from a tree yields an empty annotations list, and writing a module specification that carries annotations MUST fail with invalid_distribution_shape ("module annotations cannot be written to a document tree") rather than dropping them silently. A bead tracks adding an annotations member to the module manifest.

Write-time truncation and its failure​

A writer measures pathBudget in characters from the distribution root, on the physical path, extension included — pkg/my-org/my-project/domain/user.type.yaml, not the logical path. When a physical path exceeds the budget, the writer keeps the longest prefix of the escaped stem that fits, drops any trailing - or _, and appends __ plus the first eight hex digits of the SHA-256 of the untruncated escaped stem, recording name → stem in the module's fileNames (decisions 0001 and 0012).

Truncation MUST fail, rather than produce an unreadable tree, when:

  • even the shortest truncated form — __ plus eight hex digits plus the .type/.value suffix and the profile's extension — does not fit the budget;
  • the module directory or the package directory alone already exceeds the budget;
  • a truncated stem collides with another stem in the same module and kind.

Each failure is invalid_distribution_shape, naming the offending path and the budget. Silently overwriting a colliding file, or emitting a path over budget, is not conforming.

Field Details​

formatVersion​

formatVersion follows the shared v3-and-later contract.

Type: Integer 4 or an exact v4 release string Required: Yes Description: Integer 4 is the canonical v4.0.0 baseline. Exact strings such as "4.1.0" identify later v4 revisions. The noncanonical baseline string "4.0.0" is also accepted. Prerelease and build metadata are rejected.

Canonical baseline:

"formatVersion": 4

Accepted exact later revision:

"formatVersion": "4.1.0"

name​

Type: Name (canonical string format)
Required: Yes (in *.type.json and *.value.json files)
Description: The canonical name of the type or value

Format: Segments joined by -, where a segment is all-lowercase (a word) or all-uppercase (an initialism)

  • "user"
  • "get-user-by-email"
  • "value-in-USD"
  • "user-ID"

Constraint: escape(name) must equal the filename without its suffix. The name itself is not the filename: user-ID is stored in user-_id.type.json. See Naming for the escape and for the fileNames map that a module carries when a stem is truncated for path length.

path / module​

Type: ModuleName (canonical path format)
Required: Yes (in module.json, either field)
Description: Module path

Format: Forward-slash separated segments

  • "domain"
  • "my-org/domain"
  • "domain/orders/shipping"

Note: path and module are equivalent; path is preferred for new files, while module is accepted for backwards compatibility with legacy files.

access (module manifest)​

Type: "Public" or "Private" Required: No Default: "Public" Description: The module's visibility inside its package definition. Written only when the value is "Private", so a public module's manifest is unchanged. Placed immediately after path.

formatVersion: 4
path: domain
access: Private
types: [user]
values: []

doc​

Type: String or Array of Strings
Required: No
Description: Documentation

Formats:

  • Single-line: "doc": "Brief description"
  • Multi-line: "doc": ["Line 1", "Line 2", "Line 3"]

Location: Inside def or spec, first beside the variant (decision 0010). Not at the top level of a node file.

def​

Type: Object
Required: Yes (if this is a definition file)
Description: Type or value definition (implementation)

For Type Definitions:

  • Must contain exactly one of: TypeAliasDefinition, CustomTypeDefinition, IncompleteTypeDefinition
  • Must include access field ("Public" or "Private") when part of PackageDefinition
  • May include doc field

For Value Definitions:

  • Must contain wrapper object with exactly one of: ExpressionBody, NativeBody, ExternalBody, IncompleteBody
  • Must include access field ("Public" or "Private") when part of PackageDefinition
  • May include doc field

Example (Type):

{
"def": {
"access": "Public",
"doc": "User type",
"TypeAliasDefinition": {
"typeParams": [],
"typeExp": "morphir/SDK:string#string"
}
}
}

Example (Value):

{
"def": {
"access": "Public",
"doc": "Create a user",
"ExpressionBody": {
"inputTypes": {},
"outputType": "my-org/domain:types#user",
"body": { "Literal": { "attributes": {}, "literal": { "StringLiteral": "..." } } }
}
}
}

spec​

Type: Object
Required: Yes (if this is a specification file)
Description: Type or value specification (interface)

For Type Specifications:

  • Must contain exactly one of: TypeAliasSpecification, OpaqueTypeSpecification, CustomTypeSpecification, DerivedTypeSpecification
  • May include doc field

For Value Specifications:

  • Must contain:
    • output: Type
  • May include doc field
  • May include inputs: Object mapping parameter names to types; omitted when empty, accepted when written empty

Example (Type):

{
"spec": {
"doc": "Integer type",
"OpaqueTypeSpecification": {}
}
}

Example (Value):

{
"spec": {
"doc": "Add two integers",
"inputs": {
"a": "morphir/SDK:basics#int",
"b": "morphir/SDK:basics#int"
},
"output": "morphir/SDK:basics#int"
}
}

Access Control​

Access is recorded at two levels. A module's own visibility lives in its module manifest as the optional access member, written only when the module is Private. The visibility of each type and value lives in its definition file, as below.

In PackageDefinition Context​

When type/value files are part of a PackageDefinition:

  • def objects must include access field
  • Values: "Public" or "Private"
  • Determines visibility within the package

In PackageSpecification Context​

When type/value files are part of a PackageSpecification:

  • Only public items are included
  • spec objects do not include access field (specs are always public)

Advanced Examples​

Incomplete Type Definition​

user.type.json (with incomplete definition):

{
"formatVersion": 4,
"name": "user",
"def": {
"access": "Public",
"IncompleteTypeDefinition": {
"typeParams": [],
"reason": {
"UnresolvedReference": {
"target": "my-org/domain:types#missing-type"
}
}
}
}
}

External Value Definition​

external-api.value.json:

{
"formatVersion": 4,
"name": "call-external-api",
"def": {
"access": "Public",
"ExternalBody": {
"inputTypes": {
"url": "morphir/SDK:string#string",
"payload": "morphir/SDK:json#json"
},
"outputType": { "Reference": ["morphir/SDK:result#result", "morphir/SDK:json#json", "morphir/SDK:string#string"] },
"externalInfo": {
"provider": "http",
"endpoint": "/api/v1/data",
"method": "POST"
}
}
}
}

Value with Complex Expression Body​

calculate-total.value.json:

{
"formatVersion": 4,
"name": "calculate-total",
"def": {
"access": "Public",
"doc": [
"Calculate the total price of an order including tax.",
"Applies discounts and regional tax rates."
],
"ExpressionBody": {
"inputTypes": {
"order": "my-org/domain:orders#order",
"tax-rate": "morphir/SDK:basics#float"
},
"outputType": "morphir/SDK:basics#float",
"body": {
"LetDefinition": {
"name": "subtotal",
"definition": {
"ExpressionBody": {
"inputTypes": {},
"outputType": "morphir/SDK:basics#float",
"body": {
"Apply": {
"function": {
"Reference": {
"fqname": "morphir/SDK:list#sum"
}
},
"argument": {
"Field": {
"target": {
"Variable": {
"name": "order"
}
},
"name": "line-items"
}
}
}
}
}
},
"in": {
"Apply": {
"function": {
"Reference": {
"fqname": "morphir/SDK:basics#multiply"
}
},
"argument": {
"Tuple": {
"elements": [
{
"Variable": {
"name": "subtotal"
}
},
{
"Apply": {
"function": {
"Reference": {
"fqname": "morphir/SDK:basics#add"
}
},
"argument": {
"Tuple": {
"elements": [
{
"Literal": { "FloatLiteral": 1.0 }
},
{
"Variable": {
"name": "tax-rate"
}
}
]
}
}
}
}
]
}
}
}
}
}
}
}
}
}

Type with Type Parameters​

result.type.json:

{
"formatVersion": 4,
"name": "result",
"spec": {
"doc": "Result type representing success or error",
"CustomTypeSpecification": {
"typeParams": ["ok", "err"],
"value": {
"constructors": {
"ok": [["value", "ok"]],
"err": [["error", "err"]]
}
}
}
}
}

Module with Mixed Styles​

module.json (manifest style with some inline definitions):

{
"formatVersion": 4,
"path": "my-org/domain",
"doc": "Domain model",
"types": ["user", "order"],
"values": {
"create-user": {
"access": "Public",
"ExpressionBody": {
"inputTypes": {
"email": "morphir/SDK:string#string"
},
"outputType": "my-org/domain:types#user",
"body": {
"Constructor": {
"attributes": {},
"fqname": "my-org/domain:types#user",
"args": [
{
"Variable": {
"attributes": {},
"name": "email"
}
}
]
}
}
}
}
}
}

Note: While mixing styles is technically possible, it's recommended to use consistent style per module for clarity.

Complete Examples​

Complete Module Structure​

Directory: .morphir-dist/pkg/my-org/my-project/domain/

module.json:

{
"formatVersion": 4,
"path": "my-org/domain",
"doc": "Domain model for the application",
"types": ["user", "user-ID", "order"],
"values": ["get-user-by-email", "create-order", "validate-user"]
}

user.type.json:

{
"formatVersion": 4,
"name": "user",
"def": {
"access": "Public",
"doc": "Represents a user in the system",
"TypeAliasDefinition": {
"typeParams": [],
"typeExp": {
"Record": {
"fields": {
"user-id": "my-org/domain:types#user-ID",
"email": "morphir/SDK:string#string",
"created-at": "my-org/SDK:local-date-time#local-date-time"
}
}
}
}
}
}

user-_id.type.json:

{
"formatVersion": 4,
"name": "user-ID",
"def": {
"access": "Public",
"CustomTypeDefinition": {
"typeParams": [],
"access": "Public",
"value": {
"constructors": {
"user-ID": [
["id", "morphir/SDK:string#string"]
]
}
}
}
}
}

get-user-by-email.value.json:

{
"formatVersion": 4,
"name": "get-user-by-email",
"def": {
"access": "Public",
"doc": "Retrieve a user by their email address",
"ExpressionBody": {
"inputTypes": {
"email": "morphir/SDK:string#string",
"users": { "Reference": ["morphir/SDK:list#list", "my-org/domain:types#user"] }
},
"outputType": { "Reference": ["morphir/SDK:maybe#maybe", "my-org/domain:types#user"] },
"body": {
"Apply": {
"attributes": {},
"function": {
"Reference": {
"attributes": {},
"fqname": "morphir/SDK:list#find",
"args": []
}
},
"argument": {
"Variable": {
"attributes": {},
"name": "email"
}
}
}
}
}
}
}

Validation Rules​

File Naming​

A filename is the escaped stem of a Name, not the canonical name. See Naming for the escape.

  • ✅ Must be escape(name), matching ^_?[a-z0-9]+(-_?[a-z0-9]+)*(__[0-9a-f]{8})?_?$
  • ✅ Type files: *.type.json
  • ✅ Value files: *.value.json
  • ✅ Module files: module.json (exact name)
  • ✅ Manifest file: manifest.json (exact name, at root)
  • ❌ No spaces, periods, or characters outside [a-z0-9_-]
  • ❌ No uppercase letters. Case carries meaning in a canonical name but not in a filename, because a case-insensitive filesystem cannot keep value-in-USD and value-in-usd apart. The escape encodes an initialism as a _ prefix instead.

Valid Examples:

  • user.type.json ✅ (name user)
  • user-_id.type.json ✅ (name user-ID)
  • get-user-by-email.value.json ✅
  • value-in-_usd.value.json ✅ (name value-in-USD)
  • aux_.type.json ✅ (name aux, suffixed because Windows reserves the device name)
  • _con.type.json ✅ (name CON)

Invalid Examples:

  • User.type.json ❌ (uppercase; the escaped stem is always lowercase)
  • user-ID.type.json ❌ (canonical name used verbatim; escape it to user-_id)
  • user-(id).type.json ❌ (the retired parenthesized encoding)
  • user id.type.json ❌ (space)
  • user.id.type.json ❌ (period, use hyphen)
  • aux.type.json ❌ (Windows reserved device name; escape it to aux_)

Required Fields​

  • All files: formatVersion
  • Definition files: name, exactly one of def or spec
  • Module files: path or module
  • Manifest files: distribution, package

Field Consistency​

  • name field must match filename (without suffix)
  • path/module field must match directory structure
  • def and spec are mutually exclusive (exactly one required)

Example: If file is user.type.json, then name must be "user".

Example: If file is in .morphir-dist/pkg/my-org/my-project/domain/, then path should be "my-org/domain" or "my-org/my-project/domain" depending on package structure.

Type and Value Definition Validation​

Type Definitions (def in *.type.json):

  • Must contain exactly one of: TypeAliasDefinition, CustomTypeDefinition, IncompleteTypeDefinition
  • Must include access field when part of PackageDefinition
  • TypeAliasDefinition must have typeParams (array) and typeExp (type)
  • CustomTypeDefinition must have typeParams, access, and value.constructors (object)
  • IncompleteTypeDefinition must have typeParams and reason (HoleReason)

Type Specifications (spec in *.type.json):

  • Must contain exactly one of: TypeAliasSpecification, OpaqueTypeSpecification, CustomTypeSpecification, DerivedTypeSpecification
  • OpaqueTypeSpecification must be empty object {}
  • TypeAliasSpecification must have typeParams and typeExp
  • CustomTypeSpecification must have typeParams and value.constructors

Value Definitions (def in *.value.json):

  • Must contain wrapper object with exactly one of: ExpressionBody, NativeBody, ExternalBody, IncompleteBody
  • Must include access field when part of PackageDefinition
  • ExpressionBody must have inputTypes (object), outputType (type), and body (expression)
  • NativeBody must have inputTypes, outputType, and nativeInfo
  • ExternalBody must have inputTypes, outputType, and externalInfo
  • IncompleteBody must have inputTypes, outputType, and reason

Value Specifications (spec in *.value.json):

  • Must have output (type)
  • May have inputs (object mapping parameter names to types); omitted when empty, accepted when written empty
  • May have doc (string or array of strings)

Module Manifest Validation​

Manifest Style:

  • types must be array of Name strings
  • values must be array of Name strings
  • Referenced type/value files must exist in same directory

Inline Style:

  • types must be object mapping names to AccessControlled TypeDefinition
  • values must be object mapping names to AccessControlled ValueDefinition
  • Each definition must include access field

Hybrid Style (not recommended but allowed):

  • One of types/values can be array, other can be object
  • Consistency is preferred

fileNames:

  • fileNames values must match FileStem
  • Each key must be listed in types or values

Distribution Manifest Validation​

Required for all distributions:

  • formatVersion: Must satisfy the shared v3-and-later contract
  • distribution: Must be "Library", "Specs", or "Application"
  • package: Must be valid PackageName (canonical format)
  • pathBudget: Must be present (decision 0012)

Required for Application distributions:

  • entryPoints: Must be present and non-empty object
  • Each entry point must have target (FQName) and kind (EntryPointKind)

Optional but recommended:

  • version: Semantic version string
  • created: ISO 8601 timestamp
  • layout: Should be "VfsMode" for document tree distributions

Directory Structure Validation​

Package Directory:

  • Must match package field in manifest.json
  • Path: .morphir-dist/pkg/{package-path}/

Module Directory:

  • Must match path/module field in module.json
  • Path: .morphir-dist/pkg/{package-path}/{module-path}/
  • Must contain module.json file

Dependency Directory:

  • One per package listed under dependencies in manifest.json
  • Path: .morphir-dist/deps/{package-path}/@{version}/, where the segment beginning with @ ends the package path and is a bare @ while the model carries no package version (see Dependencies)
  • Below that segment, the same module and definition layout as pkg/

Definition Files:

  • Must be in module directory
  • Filename must match name field in file
  • Type files: {name}.type.json
  • Value files: {name}.value.json

Error Handling​

Common Validation Errors​

Missing Required Field:

// ❌ Missing 'name' field
{
"formatVersion": 4,
"def": { ... }
}

Name Mismatch:

// ❌ File is 'user.type.json' but name is 'order'
{
"formatVersion": 4,
"name": "order",
"def": { ... }
}

Both def and spec Present:

// ❌ Cannot have both def and spec
{
"formatVersion": 4,
"name": "user",
"def": { ... },
"spec": { ... }
}

Missing Access Field:

// ❌ Missing 'access' in def (required for PackageDefinition)
{
"formatVersion": 4,
"name": "user",
"def": {
"TypeAliasDefinition": { ... }
}
}

Invalid Entry Points:

// ❌ Application distribution missing entryPoints
{
"formatVersion": 4,
"distribution": "Application",
"package": "my-org/my-cli"
// Missing entryPoints!
}

When validation fails, provide clear error messages:

  • File naming: "Filename 'User.type.json' does not match canonical name format. Expected 'user.type.json'"
  • Name mismatch: "Name field 'order' does not match filename 'user.type.json'"
  • Missing field: "Required field 'name' is missing in type definition file"
  • Both def/spec: "Cannot have both 'def' and 'spec' fields. Use exactly one."
  • Missing access: "Definition in PackageDefinition context must include 'access' field"
  • Invalid entry points: "Application distribution must include 'entryPoints' field"

Schema Reference​

Formal JSON schemas are available in:

Metadata Files​

Package Metadata​

Package-level metadata can be stored in additional files:

Location: .morphir-dist/pkg/package-name/package.json (optional)

Purpose: Package-level metadata, dependencies, configuration

Example:

{
"formatVersion": 4,
"package": "my-org/my-project",
"version": "1.2.0",
"description": "My project description",
"dependencies": {
"morphir/SDK": "3.0.0"
},
"metadata": {
"author": "My Org",
"license": "Apache-2.0",
"repository": "https://github.com/my-org/my-project"
}
}

Note: Package metadata files (package.json) are optional and not part of the core V4 IR schema. They are documented here for reference, but implementations are not required to support them. The manifest.json file contains all essential distribution metadata required by the V4 specification. Package metadata files may be used by tooling for additional metadata, but are not validated by the core IR schema.

Module Metadata​

Module-level metadata is stored in module.json. Additional metadata can be included:

Example with extended metadata:

{
"formatVersion": 4,
"path": "my-org/domain",
"doc": "Domain model",
"types": ["user", "order"],
"values": ["create-order"],
"metadata": {
"tags": ["domain", "core"],
"deprecated": false,
"since": "1.0.0"
}
}

Metadata Fields (all optional):

  • tags: Array of string tags for categorization
  • deprecated: Boolean indicating if module is deprecated
  • since: Version when module was introduced
  • extensions: Object for tool-specific metadata

Reserved: $meta​

A top-level $meta member in any document-tree file is reserved (decision 0014). Readers ignore it and never report it as unknown; writers never emit it. It is not specified in 4.0.0.

Not part of a distribution​

session.jsonl, the daemon's transaction journal, is workspace state and is never read as part of a distribution (decision 0014).

File Format Comparison​

Classic Mode vs VFS Mode​

AspectClassic ModeVFS Mode
StructureSingle morphir-ir.json fileDirectory tree with individual files
Manifest FileNot separate (embedded in root).morphir-dist/manifest.json
Module StructureNested in distribution JSONmodule.json file per module
Type DefinitionsNested in module JSON*.type.json files
Value DefinitionsNested in module JSON*.value.json files
Use CaseSimple projects, backwards compatLarge projects, incremental updates

Complete Directory Tree Example​

.morphir-dist/
├── manifest.json
└── pkg/
└── my-org/
└── my-project/
├── domain/
│ ├── module.json
│ ├── user.type.json
│ ├── user-_id.type.json
│ ├── order.type.json
│ ├── get-user-by-email.value.json
│ └── create-order.value.json
├── api/
│ ├── module.json
│ ├── request.type.json
│ ├── response.type.json
│ └── handle-request.value.json
└── utils/
├── module.json
├── validation.type.json
└── validate-email.value.json