A Human-Friendly Data Serialization Format
UP templating provides declarative configuration composition using the same !type annotation syntax as the rest of UP. No string substitution, no logic, just clean data composition.
Template directives use !annotation syntax, consistent with UP’s type system:
vars {
app_name MyApp
port!int 8080
region us-west-2
}
# Reference with $ prefix
server {
port $vars.port
region $vars.region
}
config!base base.up
Load and inherit from a base configuration file.
server!overlay {
host production.example.com
replicas!int 10
}
Declaratively merge this block with existing configuration.
features!include [
features/beta.up
features/ha.up
]
Include and merge multiple files.
scaling!patch {
server.replicas!int 20
features.beta!bool true
}
Apply targeted modifications using path notation.
options!merge {
strategy deep
list_strategy append
}
Configure how blocks and lists are merged.
base.up:
vars {
app_name MyApp
default_port!int 8080
}
app_name $vars.app_name
version 1.0.0
server {
host 0.0.0.0
port $vars.default_port
replicas!int 2
}
database {
driver postgres
pool_size!int 20
}
features {
new_ui!bool false
beta_api!bool false
}
production.up:
config!base base.up
vars {
prod_host production.example.com
}
server!overlay {
host $vars.prod_host
port!int 443
replicas!int 10
tls_enabled!bool true
}
database!overlay {
host db.production.example.com
pool_size!int 100
ssl_enabled!bool true
}
features!overlay {
new_ui!bool true
analytics!bool true
}
Result after processing:
app_name MyApp
version 1.0.0
server {
host production.example.com
port 443
replicas 10
tls_enabled true
}
database {
driver postgres
pool_size 100
host db.production.example.com
ssl_enabled true
}
features {
new_ui true
beta_api false
analytics true
}
| Annotation | Purpose | Usage |
|---|---|---|
!base |
Inherit configuration | config!base base.up |
!overlay |
Merge block declaratively | server!overlay { } |
!include |
Include multiple files | mods!include [file1, file2] |
!patch |
Apply targeted changes | fixes!patch { path value } |
!merge |
Configure merge behavior | opts!merge { strategy deep } |
Use $vars.path to reference variables:
vars {
environment production
config {
timeout!dur 30s
retries!int 3
}
}
deployment {
env $vars.environment
timeout $vars.config.timeout
retries $vars.config.retries
}
Blocks are merged recursively:
# Base
server { host localhost, port!int 8080 }
# Overlay
server!overlay { host prod.com, tls!bool true }
# Result
server { host prod.com, port 8080, tls true }
append (default):
tags [web, api]
tags!overlay [production]
# Result: [web, api, production]
replace:
options!merge { list_strategy replace }
tags!overlay [production, v2]
# Result: [production, v2]
unique:
options!merge { list_strategy unique }
tags [web, api]
tags!overlay [api, production]
# Result: [web, api, production]
Path-based modifications:
scaling!patch {
server.replicas!int 20
server.cpu 4000m
features.beta!bool true
items[*].enabled!bool true
}
Structure:
config/
├── base.up # Common settings
├── development.up # Dev overrides
├── staging.up # Staging overrides
├── production.up # Production config
└── features/
├── beta.up
└── ha.up
base.up:
vars {
app_name MyApp
default_replicas!int 2
}
app_name $vars.app_name
server {
replicas $vars.default_replicas
}
development.up:
config!base base.up
server!overlay {
host localhost
debug!bool true
}
production.up:
config!base base.up
server!overlay {
host production.example.com
replicas!int 10
tls_enabled!bool true
}
production-with-beta.up:
config!base production.up
features!include [
features/beta.up
]
# Process template
up template process -i production.up -o output.up
# Validate template
up template validate -i production.up
# Output as JSON
up template process -i production.up --json --pretty
port!int 8080 # Type annotation
config!base base.up # Template annotation
Same !annotation pattern everywhere.
Any key can be used with template annotations:
base!base base.up
foundation!base base.up
parent!base base.up
$vars.port # Clear it's a variable reference
server.replicas!int 10 # Clear it's a type annotation
Traditional (Helm):
replicas:
tls:
Problems: String substitution, logic, type-unsafe, mixed languages.
UP:
config!base base.up
server!overlay {
replicas!int 10
tls_enabled!bool true
}
Benefits: Declarative, type-safe, pure UP syntax, no logic.
1. Load and parse all documents (base, includes, current)
↓
2. Extract variables from ALL documents (order-independent)
↓
3. Merge documents (base → includes → current)
↓
4. Apply overlays (blocks with !overlay)
↓
5. Apply patches (blocks with !patch)
↓
6. Iteratively resolve variables until convergence
↓
7. Output final configuration
Variables are resolved iteratively rather than sequentially. This means:
Example:
vars {
# These reference each other - order doesn't matter
region us-west-2
environment production
# Builds from above (even though defined first)
full_name $vars.environment-$vars.region
# Uses full_name (defined above)
deployment_id v1-$vars.full_name
# Multi-level reference
tag service-$vars.deployment_id
}
# All resolve correctly:
# full_name = "production-us-west-2"
# deployment_id = "v1-production-us-west-2"
# tag = "service-v1-production-us-west-2"
String Interpolation:
vars {
host example.com
port!int 443
protocol https
# Multiple variables in one string
url $vars.protocol://$vars.host:$vars.port
health_check $vars.url/health
}
# Result:
# url = "https://example.com:443"
# health_check = "https://example.com:443/health"
import up "github.com/uplang/spec/parsers/go/src"
engine := up.NewTemplateEngine()
doc, err := engine.ProcessTemplate("config.up")
const up = require('@up-lang/parser');
const doc = await up.template('config.up');
import up
doc = up.process_template('config.up')
All examples are in examples/templates/:
base.up - Common configurationdevelopment.up - Dev environmentstaging.up - Staging environmentproduction.up - Production environmentfeatures/*.up - Feature modulescomposed/*.up - Composed configurationsProcess them:
up template process -i examples/templates/production.up
UP supports multiple “documents” in a single file using comment-based separators. This is useful for:
Use any comment line as a separator (common convention: # ---):
# base configuration
vars { app_name MyApp }
server { host localhost, port!int 8080 }
# ---
# development overlay
config!base base.up
server!overlay { debug!bool true }
# ---
# production overlay
config!base base.up
server!overlay { host prod.com, tls!bool true }
Tooling can parse comment-separated documents and warn about:
Ordering Issues:
# BAD: overlay before base is loaded
server!overlay { tls!bool true }
# ---
config!base base.up
Undefined References:
database { host $vars.db_host } # Warning: vars.db_host not defined
# ---
vars { api_host api.example.com } # Different doc, doesn't define db_host
Circular Dependencies:
# a.up
config!base b.up
# ---
# b.up
config!base a.up
A linter can detect these by:
config-all.up:
# Base configuration - common settings
vars {
app_name MyApp
default_port!int 8080
}
app_name $vars.app_name
version 1.0.0
server {
host 0.0.0.0
port $vars.default_port
replicas!int 2
}
# ---
# Development configuration
config!base base.up
vars {
dev_host localhost
}
server!overlay {
host $vars.dev_host
debug!bool true
replicas!int 1
}
# ---
# Production configuration
config!base base.up
vars {
prod_host production.example.com
}
server!overlay {
host $vars.prod_host
replicas!int 10
tls_enabled!bool true
}
features!include [
features/monitoring.up
features/high-availability.up
]
Process a specific document (by line range, index, or extraction):
# Extract and process just the production config
sed -n '/# Production/,/^# ---$/p' config-all.up | up template process
# Or use future up multi-doc support:
up template process -i config-all.up --doc 2 # 0-indexed
UP’s templating system (!base, !overlay, !include, !patch, vars) provides complete DRY support that is far superior to YAML anchors.
# YAML anchors are fragile and limited
defaults: &defaults
adapter: postgres
host: localhost
pool_size: 20
development:
<<: *defaults # Merge is shallow
database: dev_db
production:
<<: *defaults
database: prod_db
host: prod.example.com # Easy to forget overrides, no validation
pool_size: 100 # No type checking
Issues with YAML anchors:
&anchor, *alias, <<: merge keyExample: Same functionality, better design
base.up - Clean, typed, reusable:
vars {
adapter postgres
host localhost
pool_size!int 20
}
database {
adapter $vars.adapter
host $vars.host
pool_size $vars.pool_size
}
development.up - Declarative composition:
config!base base.up
database!overlay {
name dev_db
debug!bool true
}
production.up - Type-safe, validated:
config!base base.up
vars!overlay {
host prod.example.com
pool_size!int 100
}
database!overlay {
name prod_db
host $vars.host
pool_size $vars.pool_size
ssl_enabled!bool true
}
production-ha.up - Multi-file composition:
config!base production.up
features!include [
features/high-availability.up
features/monitoring.up
features/backup.up
]
scaling!patch {
database.pool_size!int 200
database.replicas!int 3
cache.enabled!bool true
}
| Feature | YAML Anchors | UP Templating |
|---|---|---|
| Merge depth | Shallow only | Deep by default |
| Type safety | None | Full !type annotations |
| File composition | Limited | !base, !include |
| Targeted patching | No | !patch with paths |
| Variable interpolation | No | $vars.path |
| Order independence | No (anchor must be first) | Yes (iterative resolution) |
| Merge strategies | Fixed shallow | Configurable (!merge) |
| Validation | No | Type-checked |
| Syntax clarity | &, *, <<: confusing |
!annotation consistent |
| Indentation safety | Error-prone | Robust |
| Cross-file reuse | No | Yes |
1. Deep merge of nested configuration:
YAML (requires manual repetition):
defaults: &defaults
server:
timeout: 30
retries: 3
database:
pool_size: 20
# Can't deep merge - must repeat entire structure
production:
server:
timeout: 30 # Repeated
retries: 3 # Repeated
host: prod.com # New value
database:
pool_size: 100 # Changed
# Missing timeout and retries - error prone!
UP (clean deep merge):
# base.up
server {
timeout!dur 30s
retries!int 3
}
database {
pool_size!int 20
}
# production.up
config!base base.up
server!overlay {
host prod.example.com
}
database!overlay {
pool_size!int 100
}
# Result: All base values preserved, only specified values changed
2. Multi-file composition:
YAML (limited):
# No native multi-file composition
# Must use external tools like Helm, Kustomize
UP (native):
config!base common.up
features!include [
team/feature-a.up
team/feature-b.up
vendor/integration-c.up
]
3. Targeted modifications of deeply nested values:
YAML (verbose and error-prone):
# Must repeat entire structure to change one nested value
app:
server:
cache:
redis:
host: redis.prod.com # Only want to change this
port: 6379 # But must repeat all siblings
pool_size: 50
timeout: 5000
UP (surgical):
config!base base.up
fixes!patch {
app.server.cache.redis.host redis.prod.com
}
# Only the targeted value changes, everything else preserved
4. Type-safe variable composition:
YAML (no type safety):
defaults: &defaults
port: "8080" # Oops, string instead of number
replicas: true # Oops, bool instead of number
production:
<<: *defaults
# Inherits type errors!
UP (type-checked):
vars {
port!int 8080 # Type enforced
replicas!int 3 # Type enforced
}
server {
port $vars.port
replicas $vars.replicas
}
# Type errors caught at parse/validation time
5. Order-independent variable resolution:
YAML (order matters):
# Must define anchor before use
base: &base
url: &base_url "http://localhost"
app:
<<: *base
full_url: *base_url # Can only alias, not compose
UP (order-independent):
vars {
# These can reference each other in any order
full_url $vars.protocol://$vars.host:$vars.port
protocol https
host example.com
port!int 443
health_check $vars.full_url/health
}
# Iterative resolution handles any order
6. Environment-specific composition with validation:
YAML (no validation):
# base.yaml
defaults: &defaults
replicas: 2
# production.yaml
production:
<<: *defaults
replicas: "many" # Type error not caught!
UP (validated):
# base.up
server {
replicas!int 2
}
# production.up
config!base base.up
server!overlay {
replicas!int many # Parse error: "many" is not an integer
}
Declarative, not repetitive:
Type-safe by design:
Composable across files:
Clear, predictable semantics:
<<: merge behavior is surprising!overlay and !patch are explicitNo indentation hell:
Conclusion: For any non-trivial configuration, UP’s templating system provides superior DRY capabilities, type safety, and maintainability compared to YAML anchors.
No templating hell. Just clean, composable configuration.