It always starts innocently, you're creating a new config or endpoint with some dynamic behavior, thinking "KISS - 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 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 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, etc. into a single document at any time, add the logic that belongs, and send it anywhere. Export portions to the 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', doc.get('prod') as string));
/*
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 that memory, because JSON Schema has no concepts of units, the best you can do is write a comment and hope the user reads it. The validation logic for that field lives somewhere else entirely, probably in your application code.
In Stof, validation lives on the fields themselves:
#[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
}
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, so pass "2GB" and it converts, pass in "100MB" and it fails. The schema can't drift from the data because it is the data.
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 when it gets past the toy stage.
Give it a try
The fastest way in is the playground, runs in your browser via WASM, no install needed.
npm i @formata/stof # TypeScript/JavaScript
pip install stof # Python
cargo install stof-cli # CLI
There's also a VSCode extension for Stof syntax highlighting.
Apache 2.0.
Top comments (0)