Skip to main content

Document Type

Status: In v4.0.0 per decision 0013; the SDK exposes construction, inspection and navigation only (no merge, equality, or ordering).

The Document type provides a first-class, schema-less JSON-like data structure within the Morphir IR for representing untyped or dynamically-typed data.

Design Principles​

  • Value-Level Concern: Document is a Literal variant, not a Type variant
  • SDK Type: The type is morphir/SDK:document#document - a built-in reference
  • Platform Native: Operations are specs-only; backends implement natively
  • JSON-Compatible: Structure maps directly to JSON for easy interop

Use Cases​

Use CaseDescription
External APIsResponses where schema is unknown or varies
ConfigurationSettings that don't need strict compile-time typing
ForwardingPass-through opaque data without interpretation
MetadataContext-dependent annotations
InteropBridge to JSON-based systems

Gleam Type Definitions​

Document Value​

// === document.gleam ===

/// Recursive document value structure
/// Represents JSON-like schema-less data
pub type DocumentValue {
/// JSON null
DocNull

/// JSON boolean
DocBool(value: Bool)

/// JSON number (integer)
DocInt(value: Int)

/// JSON number (floating point)
DocFloat(value: Float)

/// JSON string
DocString(value: String)

/// JSON array (ordered list)
DocArray(elements: List(DocumentValue))

/// JSON object (string-keyed map)
DocObject(fields: Dict(String, DocumentValue))
}

Literal Extension​

The DocumentLiteral variant is added to the existing Literal type:

// In values.gleam - extended Literal type

pub type Literal {
// Existing literals...
StringLiteral(value: String)
IntLiteral(value: Int)
FloatLiteral(value: Float)
BoolLiteral(value: Bool)
CharLiteral(value: String)
IntegerLiteral(value: Int)
DecimalLiteral(value: String)

/// Document literal - schema-less JSON-like tree
DocumentLiteral(value: DocumentValue)
}

Type Reference​

Document values have the type morphir/SDK:document#document:

// The Document type is a simple opaque type in the SDK
// No new Type variant needed - it's just a Reference

fn document_type() -> Type(a) {
Reference(
attributes: default_attributes(),
fqname: fqname_from_string("morphir/SDK:document#document"),
args: [],
)
}

JSON Serialization​

Decision 0013 settled the encoding: the payload of DocumentLiteral is the document itself, verbatim. There are no Doc* wrappers on the wire and no { "value": ... } spelling, so { "DocumentLiteral": { "value": 1 } } is the one-member document {"value": 1}.

{ "DocumentLiteral": { "name": "Alice", "age": 30, "tags": ["admin", "user"], "metadata": null } }

Readers preserve number lexemes (9007199254740993 and 0.10 survive a round trip); a binding that narrows them to a 64-bit float fails kit case patterns-and-literals-0006. A DocumentLiteral cannot appear in a LiteralPattern (patterns-and-literals-0007), and a v4 to v3 downgrade of a document refuses with unsupported_v4_downgrade.

The in-memory DocumentValue above is one binding's choice of representation; the wire format does not depend on it.

SDK Specification​

The Document SDK provides operations as specifications (native implementation per platform).

Type Definition​

module Morphir.SDK.Document exposing
( Document
, null, bool, int, float, string, array, object
, asBool, asInt, asFloat, asString, asArray, asObject
, get, getPath
, isNull, isBool, isInt, isFloat, isString, isArray, isObject
, encode, decoder
)

{-| Schema-less document type for representing JSON-like data.

@docs Document

## Construction
@docs null, bool, int, float, string, array, object

## Extraction
@docs asBool, asInt, asFloat, asString, asArray, asObject

## Navigation
@docs get, getPath

## Predicates
@docs isNull, isBool, isInt, isFloat, isString, isArray, isObject

## Encoding/Decoding
@docs encode, decoder

-}

Construction Functions​

{-| The null document value. -}
null : Document

{-| Create a document from a boolean. -}
bool : Bool -> Document

{-| Create a document from an integer. -}
int : Int -> Document

{-| Create a document from a float. -}
float : Float -> Document

{-| Create a document from a string. -}
string : String -> Document

{-| Create a document from a list of documents. -}
array : List Document -> Document

{-| Create a document from key-value pairs. -}
object : List ( String, Document ) -> Document

Extraction Functions​

{-| Extract a boolean, if the document is a boolean. -}
asBool : Document -> Maybe Bool

{-| Extract an integer, if the document is an integer. -}
asInt : Document -> Maybe Int

{-| Extract a float, if the document is a number.
Integers are coerced to floats.
-}
asFloat : Document -> Maybe Float

{-| Extract a string, if the document is a string. -}
asString : Document -> Maybe String

{-| Extract an array, if the document is an array. -}
asArray : Document -> Maybe (List Document)

{-| Extract an object as a dictionary, if the document is an object. -}
asObject : Document -> Maybe (Dict String Document)
{-| Get a field from a document object by key.
Returns Nothing if not an object or key doesn't exist.

get "name" (object [("name", string "Alice")]) == Just (string "Alice")
get "age" (string "hello") == Nothing
-}
get : String -> Document -> Maybe Document

{-| Get a nested value by path.

doc = object
[ ("user", object
[ ("profile", object
[ ("name", string "Alice") ]
)
]
)
]

getPath ["user", "profile", "name"] doc == Just (string "Alice")
getPath ["user", "missing"] doc == Nothing
-}
getPath : List String -> Document -> Maybe Document

Predicate Functions​

{-| Check if document is null. -}
isNull : Document -> Bool

{-| Check if document is a boolean. -}
isBool : Document -> Bool

{-| Check if document is an integer. -}
isInt : Document -> Bool

{-| Check if document is a float (non-integer number). -}
isFloat : Document -> Bool

{-| Check if document is a string. -}
isString : Document -> Bool

{-| Check if document is an array. -}
isArray : Document -> Bool

{-| Check if document is an object. -}
isObject : Document -> Bool

Code Generation​

TypeScript/JavaScript​

// Document maps to `unknown` or a branded type
type Document = unknown;

// Or with runtime type info
type Document =
| null
| boolean
| number
| string
| Document[]
| { [key: string]: Document };

// SDK functions
const Document = {
null: null,
bool: (b: boolean): Document => b,
int: (n: number): Document => n,
string: (s: string): Document => s,
array: (arr: Document[]): Document => arr,
object: (entries: [string, Document][]): Document =>
Object.fromEntries(entries),

asString: (doc: Document): string | undefined =>
typeof doc === 'string' ? doc : undefined,

get: (key: string, doc: Document): Document | undefined =>
typeof doc === 'object' && doc !== null && !Array.isArray(doc)
? (doc as Record<string, Document>)[key]
: undefined,
};

Scala​

// Document as a sealed trait or type alias
sealed trait Document
case object DocNull extends Document
case class DocBool(value: Boolean) extends Document
case class DocInt(value: Long) extends Document
case class DocFloat(value: Double) extends Document
case class DocString(value: String) extends Document
case class DocArray(elements: List[Document]) extends Document
case class DocObject(fields: Map[String, Document]) extends Document

// Or use existing JSON library
import io.circe.Json
type Document = Json

Go​

// Document as interface{} or any
type Document = any

// Or with explicit types
type Document interface {
isDocument()
}

type DocNull struct{}
type DocBool struct{ Value bool }
type DocInt struct{ Value int64 }
type DocFloat struct{ Value float64 }
type DocString struct{ Value string }
type DocArray struct{ Elements []Document }
type DocObject struct{ Fields map[string]Document }

Java​

// Using sealed interfaces (Java 17+)
public sealed interface Document permits
DocNull, DocBool, DocInt, DocFloat, DocString, DocArray, DocObject {}

public record DocNull() implements Document {}
public record DocBool(boolean value) implements Document {}
public record DocInt(long value) implements Document {}
public record DocFloat(double value) implements Document {}
public record DocString(String value) implements Document {}
public record DocArray(List<Document> elements) implements Document {}
public record DocObject(Map<String, Document> fields) implements Document {}

VFS File Example​

Value Definition Using Document​

File: .morphir-dist/pkg/my-org/api/values/parse-response.value.json

{
"formatVersion": "4.0.0",
"name": "parse-response",
"def": {
"access": "Public",
"value": {
"ExpressionBody": {
"inputTypes": {
"response": "morphir/SDK:document#document"
},
"outputType": ["morphir/SDK:maybe#maybe", "my-org/api:types#user"],
"body": {
"Apply": {
"function": { "Reference": { "fqname": "morphir/SDK:maybe#and-then" } },
"args": [
{
"Apply": {
"function": { "Reference": { "fqname": "morphir/SDK:document#get" } },
"args": [
{ "Literal": { "StringLiteral": "data" } },
{ "Variable": { "name": "response" } }
]
}
}
]
}
}
}
}
}
}

Document Literal in IR​

{
"Literal": {
"DocumentLiteral": {
"type": "user",
"attributes": {
"id": 12345,
"roles": ["admin", "editor"],
"settings": {
"theme": "dark",
"notifications": true
}
}
}
}
}

Comparison with Alternatives​

ApproachProsCons
Document (this design)Clean, JSON-native, simpleNo compile-time structure checking
Type variantType-level operationsComplicates Type sum type
Extensible recordsSome structureStill needs known fields
Any/DynamicMaximum flexibilityNo structure at all

Open Considerations​

Optional Extensions​

These are not part of the core design but could be added later:

  1. Binary data: DocBinary(bytes: BitArray) for base64-encoded blobs
  2. Timestamps: DocTimestamp(value: String) for ISO 8601 dates
  3. Big numbers: DocDecimal(value: String) for arbitrary precision
  4. IR references: DocReference(fqname: FQName) for linking to IR nodes

Schema Validation​

Runtime schema validation could be provided via decorations:

{
"my-org/api:handlers#response": {
"schema": {
"type": "object",
"properties": {
"data": { "type": "object" },
"error": { "type": "string" }
}
}
}
}

Merge Semantics​

For combining documents (e.g., in configuration):

{-| Merge two documents. Objects are deep-merged, other types use the second value. -}
merge : Document -> Document -> Document