The Problem With JSON Schema (And What We Did Differently)
JSON Schema's core limitation isn't its syntax — it's that a schema is always a second, separate document describing the shape of a first one, and nothing forces the two to stay in agreement. Add a field to your data and forget the schema, and nothing tells you. Stof takes a different approach: #[schema(...)] attaches a validation rule directly to the field it protects, in the same document, so there's no second file that can quietly fall out of sync.
Where JSON Schema Actually Struggles
It's a separate file that has to be kept in sync by hand. The schema and the data it validates live in different places, maintained by whoever remembers to update both. There's no mechanism that enforces the connection — just discipline, which is exactly the kind of thing that erodes under deadline pressure.
It has no concept of units. A field documented as "memory in MB, must be at least 256" is a string with a comment, not an enforced rule — JSON Schema can check that the value is a number, but has no way to know that "2GB" and 256000000 describe the same thing, or that a value in KB shouldn't be silently accepted where MB was intended.
Cross-field validation is awkward at best. Checking that one field's value is consistent with another's needs $data references or vendor-specific extensions that most tooling doesn't fully support — the schema starts working against its own format to express something that would be a one-line comparison in real code.
The validation logic usually gets duplicated anyway. Even with a schema in place, most teams end up writing the same checks again in application code — for error messages the schema can't produce, for logic too complex to express declaratively, or just because it's faster than fighting the schema format for an edge case.
None of this makes JSON Schema poorly designed for what it targets: validating the shape of arbitrary JSON, in a language-agnostic, toolable way. The problem is structural, not an implementation detail — it's what happens when validation has to live somewhere other than the data itself.
Validation as Part of the Data
In Stof, #[schema(...)] is an attribute on the field it validates:
#[type]
Plan: {
#[schema((target_value: str): bool => target_value.len() > 0)]
str label: 'Growth Plan'
#[schema((target_value: hr): bool => target_value > 0hr)]
hr reset_inc: 1hr
}
schemafy(target) checks a target object against every field that has one, and the unit-typed field actually means something — 1hr and 60min are the same value as far as the rule is concerned, and a plain number with no unit doesn't silently pass as if it had the right one.
Beyond a Single Rule
A field's #[schema(...)] doesn't have to be one function. A list runs as a pipeline, each check short-circuiting on the first failure:
#[schema((
(target_value: unknown): bool => (typeof target_value) == 'str',
(target_value: str): bool => target_value.contains('@'),
))]
email: 'someone@example.com'
A bare #[schema] with no function tells schemafy to recurse into that field if the target's value is an object — validating nested structure without hand-writing traversal logic. And schemafy(target, remove_invalid = true) doesn't just report a failure; it strips whatever didn't pass, turning validation into cleanup in the same step. remove_undefined = true does the same for fields the schema never mentioned at all — filtering and renaming as a batch, not a separate pass.
None of this requires a second file, a different syntax to learn on top of the data format, or a build step to keep generated types in sync. It's the same document, doing one more thing.
If you want to see this built into a complete, runnable example rather than isolated snippets, A Self-Validating Config walks through the whole thing end to end, and Prototypes & Schemas is the full reference.
FAQ
Does Stof replace JSON Schema entirely?
For validating a Stof document, yes — #[schema] is the built-in mechanism and doesn't need JSON Schema alongside it. If you're validating plain JSON from a system that isn't Stof-aware, JSON Schema is still a reasonable tool for that narrower job.
Can a Stof schema validate data that didn't originate as Stof?
Yes. schemafy(target) works on any object in the graph, including one built from parse(json, target, 'json') — the target doesn't need to have been created as an instance of the schema's own prototype.
What happens when validation fails?
schemafy returns false rather than throwing, so a failed validation is a value you check, not an exception you have to catch. Combined with remove_invalid, it can also actively clean up the target instead of just reporting the problem.
Is this the same idea as Zod, Yup, or io-ts? Similar goal — colocating validation with a type definition instead of a separate schema file — but those libraries validate data at the boundary of a TypeScript application. Stof's schemas live inside the document itself, portable to any host that runs Stof, not tied to one language's type system.
