A Human-Friendly Data Serialization Format
Every UP document has a deterministic, canonical JSON representation. This document explains the relationship between UP and JSON, including data type mappings, ordering semantics, and bidirectional conversion.
UP is a superset of JSON’s data model with better syntax and explicit ordering semantics. Any JSON document can be represented in UP, and any UP document can be converted to JSON.
| UP | JSON | Notes |
|---|---|---|
| Scalar values | Strings, numbers, booleans | Direct mapping |
key value |
"key": "value" |
String by default |
key!int 42 |
"key": 42 |
Type annotations guide JSON types |
key!bool true |
"key": true |
Boolean |
key {} |
"key": {} |
Object (ordered by key) |
key!list {} |
"key": {} |
Object (insertion-order) |
key [] |
"key": [] |
Array |
| Multiline strings | Strings with \n |
Whitespace preserved |
| Comments | omitted | Comments don’t appear in JSON |
JSON specification (RFC 8259) states that objects are unordered collections:
{
"port": 8080,
"host": "localhost",
"debug": true
}
While many implementations preserve insertion order, it’s not guaranteed. Two JSON documents with the same keys in different order are semantically equivalent.
UP provides two types of maps with explicit ordering semantics:
{})Default UP blocks are ordered by key alphabetically for deterministic output:
server {
port!int 8080
host localhost
debug!bool true
}
Converts to JSON with keys sorted:
{
"server": {
"debug": true,
"host": "localhost",
"port": 8080
}
}
Benefits:
!list {})For cases where insertion order matters, use !list annotation:
steps!list {
checkout git clone ...
build make build
test make test
deploy ./deploy.sh
}
Preserves insertion order in JSON:
{
"steps": {
"checkout": "git clone ...",
"build": "make build",
"test": "make test",
"deploy": "./deploy.sh"
}
}
Characteristics:
Syntax options:
# These are equivalent - insertion-ordered maps
pipeline!list {}
pipeline!ordered {}
pipeline!seq {}
UP:
app_name MyService
version 1.2.3
server {
port!int 8080
host 0.0.0.0
timeout!dur 30s
}
database {
driver postgres
host db.internal
pool_size!int 20
}
features {
new_ui!bool true
beta_api!bool false
}
JSON (canonical, keys sorted):
{
"app_name": "MyService",
"database": {
"driver": "postgres",
"host": "db.internal",
"pool_size": 20
},
"features": {
"beta_api": false,
"new_ui": true
},
"server": {
"host": "0.0.0.0",
"port": 8080,
"timeout": "30s"
},
"version": "1.2.3"
}
Note: All values without type annotations are strings by default.
UP:
pipeline!list {
init {
command npm install
timeout!int 300
}
lint {
command npm run lint
continue_on_error!bool true
}
test {
command npm test
required!bool true
}
build {
command npm run build
artifacts [dist, build]
}
deploy {
command ./deploy.sh
environment production
}
}
JSON (insertion order preserved):
{
"pipeline": {
"init": {
"command": "npm install",
"timeout": 300
},
"lint": {
"command": "npm run lint",
"continue_on_error": true
},
"test": {
"command": "npm test",
"required": true
},
"build": {
"artifacts": ["dist", "build"],
"command": "npm run build"
},
"deploy": {
"command": "./deploy.sh",
"environment": "production"
}
}
}
Note: Within each step block (init, lint, etc.), keys are still sorted unless those blocks are also marked !list.
UP:
# Top level: key-ordered (default)
workflow_name MyWorkflow
# Jobs preserve insertion order
jobs!list {
setup {
runs_on ubuntu-latest
steps!list {
checkout actions/checkout@v2
install npm install
}
}
test {
runs_on ubuntu-latest
needs [setup]
steps!list {
test npm test
coverage npm run coverage
}
}
deploy {
runs_on ubuntu-latest
needs [test]
steps!list {
build npm run build
deploy ./deploy.sh
}
}
}
# Config at top level: key-ordered
config {
timeout_minutes!int 30
max_parallel!int 3
}
UP type annotations guide JSON type conversion. String is the default - only specify types for non-strings:
# Strings (default - no annotation needed)
name Alice
host localhost
version 1.2.3
# Multi-word strings (use quotes or colon suffix)
title "Software Engineer"
description: A modern data serialization format
# Numbers (annotation required)
port!int 8080
timeout!float 30.5
cpu!number 2.5
# Booleans (annotation required)
enabled!bool true
debug!boolean false
# Null (annotation required)
value!null null
# Arrays
tags [web, api, production]
ports [8080, 8443, 9090]
# Nested objects
server {
config {
timeout!int 30
}
}
JSON:
{
"cpu": 2.5,
"debug": false,
"description": "A modern data serialization format",
"enabled": true,
"host": "localhost",
"name": "Alice",
"port": 8080,
"ports": [8080, 8443, 9090],
"server": {
"config": {
"timeout": 30
}
},
"tags": ["web", "api", "production"],
"timeout": 30.5,
"value": null
}
UP multiline strings convert to JSON strings with newlines:
UP:
description ```
This is a multiline
string that preserves
whitespace and newlines
code!python ```python def hello(): print(“world”)
JSON:
{
"code": "def hello():\n print(\"world\")",
"description": "This is a multiline\nstring that preserves\nwhitespace and newlines"
}
UP tables convert to array of objects:
UP:
users!table {
columns [id, name, email]
rows {
[1, Alice, alice@example.com]
[2, Bob, bob@example.com]
[3, Carol, carol@example.com]
}
}
JSON:
{
"users": [
{"id": 1, "name": "Alice", "email": "alice@example.com"},
{"id": 2, "name": "Bob", "email": "bob@example.com"},
{"id": 3, "name": "Carol", "email": "carol@example.com"}
]
}
Converting JSON to UP requires choosing ordering strategy:
Default (key-ordered):
up parse input.json -o output.up --order-keys
Preserve insertion order:
up parse input.json -o output.up --preserve-order
UP to JSON is deterministic:
up parse input.up --json --pretty
Output is always:
!list blocks)UP’s canonical form ensures:
Example:
These two UP documents are semantically equivalent and produce identical JSON:
# Version A
server { port!int 8080, host localhost }
# Version B - different formatting, whitespace
server {
host localhost
port!int 8080
}
Both produce:
{"server": {"host": "localhost", "port": 8080}}
| Use Case | Ordering | Syntax |
|---|---|---|
| Configuration files | Key-ordered | {} |
| Feature flags | Key-ordered | {} |
| Environment variables | Key-ordered | {} |
| Pipelines/workflows | Insertion-order | !list {} |
| Sequential steps | Insertion-order | !list {} |
| Ordered operations | Insertion-order | !list {} |
| Migration scripts | Insertion-order | !list {} |
{}: Store as ordered map, output sorted by key!list {}: Store as ordered map, output in insertion order[]: Always preserve order (standard array behavior)type Block map[string]interface{} // Key-ordered by default
type OrderedBlock []KeyValue // Insertion-order for !list
type List []interface{} // Arrays, always ordered
type KeyValue struct {
Key string
Value interface{}
}
// Key-ordered (default)
func (b Block) MarshalJSON() ([]byte, error) {
keys := make([]string, 0, len(b))
for k := range b {
keys = append(keys, k)
}
sort.Strings(keys) // Sort alphabetically
// ... marshal in order
}
// Insertion-ordered (!list)
func (b OrderedBlock) MarshalJSON() ([]byte, error) {
// Marshal in the order items appear
// ... preserve insertion order
}
# Validate UP-JSON round-trip
up parse input.up --json | up parse --format up
# Compare JSON outputs
up parse a.up --json > a.json
up parse b.up --json > b.json
diff a.json b.json
# Generate JSON schema from UP
up schema input.up -o schema.json
| Feature | JSON | UP Default {} |
UP !list {} |
|---|---|---|---|
| Key ordering | Unspecified | Alphabetical | Insertion order |
| Deterministic | No | Yes | Yes |
| Unique keys | Yes | Yes | Yes |
| Use case | Data exchange | Configs, settings | Pipelines, sequences |
| Diff-friendly | Sometimes | Always | Always |
| Syntax | {} |
{} |
!list {} |
UP provides the clarity JSON lacks: explicit ordering semantics for deterministic, diffable, mergeable configurations.
UP achieves type safety with simplicity:
!int, !bool, etc. for non-strings1.2.3 is always a string unless annotated!int 42This approach means:
123 a string or number?)version 1.2.3 vs port!int 8080