We Deserve Better Than JSON as a DSL
It always starts innocently. You're creating a new config or endpoint with some dynamic behavior, thinking "keep it stupidly simple." What's simpler than JSON or TOML? Some key-value pairs, maybe some nesting — we'll actually keep it clean this time.
Any experienced programmer knows what comes next.
Six months later you're hunting for a $ref in a sea of JSON, writing helper tools, trapped under a stack of legacy decisions, full of regret.
I've done it. You've done it. The entire industry has done it. GitHub Actions is YAML with a custom expression language bolted on. Terraform invented HCL because JSON wasn't expressive enough. OpenAPI is JSON Schema with extensions piled on top. Every AI framework has its own JSON-based tool definition format that's slightly different from the others.
The problem isn't the formats. JSON, YAML, TOML — they're all fine at what they do. The problem is that we keep asking them to carry logic they were never designed to hold, then building increasingly elaborate scaffolding when they inevitably buckle.
What if your data could just do what's needed?
Here's the part that made me want to build a real data runtime — Stof.
import { stofAsync } from '@formata/stof';
const doc = await stofAsync`
name: 'Stof'
fn loaded() -> str {
const stof = await Ext.fetch();
parse(stof, self);
self.say_hello()
}
`;
doc.lib('Ext', 'fetch', async () => {
return `fn say_hello() -> str {
'Hello, ' + (self.name ?? 'World') + '!'
}`;
});
console.log(await doc.call('loaded')); // Hello, Stof!
The document starts without a say_hello function. It fetches more Stof from somewhere — an API, another service, an agent — then parses it into itself and calls the function that just arrived.
Stof runs in a WASM sandbox built in Rust and is just a document of data, like JSON (actually a superset of JSON). It can't touch your filesystem, network, or memory unless you explicitly bridge it to the host environment with doc.lib(). You control exactly what the context can reach.
This means a service can share its capabilities as Stof. Not a description of what it can do, but the actual logic. The consumer parses it into context and starts using it immediately — no client library, no SDK, no redeployment. Your system ships with certain capabilities and gains more at runtime.
The Whole Picture
Stof is a superset of JSON, so your existing data is already valid — but with functions, types, unit conversions, and async execution built into the format itself, instead of a layer bolted on top.
Instead of trying to replace existing interchange formats, Stof is the glue layer that works with all of them. Parse JSON, YAML, TOML, Stof, or more into a single document at any time, add the logic that belongs, and send it anywhere. Export portions to whichever format your app expects internally.
const doc = await stofAsync`
#[type]
Server: {
port: 8080
host: 'localhost'
secure: false
MiB memory: 500GiB
fn url() -> str {
let url = self.secure ? 'https://' : 'http://';
url += self.host + ':' + self.port;
url
}
}`;
// Parse JSON, YAML, TOML, binary, or more Stof into the same document
doc.parse(`Server "prod": {
"host": "prod.example.com",
"port": 443,
"secure": true,
"memory": "2GB"
}`);
console.log(await doc.call('prod.url')); // https://prod.example.com:443
console.log(doc.get('prod.memory')); // ~1907 MiB (auto-converted from GB)
console.log(doc.stringify('toml', 'prod'));
/*
host = "prod.example.com"
port = 443
secure = true
memory = 1907.3486328124998 # MiB
*/
The Server type defines shape, defaults, and behavior. When you parse new data in — JSON, YAML, TOML, whatever — and cast it to that type, it gets the functions and validation for free.
Schemas That Don't Drift
You know what's worse than writing a JSON Schema? Keeping it in sync with the thing it validates.
Here's the JSON Schema for a simple server config:
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"port": {
"type": "integer",
"exclusiveMinimum": 1024,
"maximum": 65536
},
"address": {
"type": "string",
"minLength": 1
},
"memory": {
"type": "string",
"description": "Memory in MB, must be at least 256"
}
},
"required": ["address"]
}
That's a separate file, a separate format, a separate thing to maintain. And notice memory — because JSON Schema has no concept of units, the best you can do is write a comment and hope the person reading it notices. The validation logic for that field lives somewhere else entirely, probably in your application code.
In Stof, validation lives on the fields themselves — and because this part is pure Stof, no TypeScript host required, you can run it right here:
#[type]
Server: {
#[schema((target_val: int): bool => target_val > 1024 && target_val <= 65536)]
int port: 8080
#[schema((target_val: str): bool => target_val != "")]
str address: "localhost"
#[schema((target_val: MiB): bool => target_val >= 256MB)]
MiB memory: 2GB
}
#[main]
fn main() {
const good = new { port: 8080, address: "prod.example.com", memory: "2GB" };
pln(self.Server.schemafy(good));
const bad = new { port: 80, address: "prod.example.com", memory: "2GB" };
pln(self.Server.schemafy(bad));
}A port between 1024 and 65536, a non-empty address, and at least 256MB of memory. The last one is meaningful because Stof understands units as types — pass "2GB" and it converts, pass "100MB" and it fails. The schema can't drift from the data, because it is the data. (More on this pattern in A Self-Validating Config, if you want to see it built out into a full recipe.)
A Real Production Use Case
Limitr is an open source pricing and enforcement engine built on Stof. The entire policy — plans, credits, limits, validation logic — lives in a single Stof document. It's a good example of what "data that carries its own logic" looks like once it's past the toy stage.
Give It a Try
The fastest way in is the playground — runs in your browser via WASM, no install needed. Every concept page in the docs has one built in too.
npm i @formata/stof # TypeScript / JavaScript
pip install stof # Python
cargo install stof-cli # CLI
- Docs — install, the full standard library, and worked examples
- GitHub — source and issues
- Discord — come tell us about your use case
There's also a VS Code extension for Stof syntax highlighting.
Apache 2.0.
