schema
Describe what valid data looks like, then check and clean untrusted values and list every problem at once.
import "std:schema"schema checks data whose shape you don't control: a request body, a config file, a form. You build a schema out of small parts (string, integer, object, array), then ask it to parse a value. A good value comes back cleaned up, a bad one is reported with the path to the problem.
Reach for it when json.parse<T> is too rigid: values that can be one of several kinds, rules such as "at least 3 characters", or error messages you want to show users.
A first schema#
Schemas are values. schema.object takes the shape you expect, and parse returns the value if it fits.
import "std:schema"
auto user = schema.object({
name: schema.string(),
age: schema.integer().min(0)
})
auto ok = user.parse({name: "Ada", age: 36})
log ok.name ok.age
log user.isValid({name: "Ada", age: -1})
log user.isValid({name: "Ada"})isValid gives a yes or no. A key that isn't marked optional is required, so a missing age fails.
Try it: change age to 36.5 and check isValid. An integer schema wants whole numbers.
Failure with parse#
parse throws a ValueError that names the path of the first problem. Wrap the call in try to get a result instead.
import "std:schema"
auto user = schema.object({name: schema.string(), age: schema.integer()})
switch try(user.parse({name: "Ada", age: "old"})) (
case ok(v)
log "fine"
case err(e)
log e.kind
log e.message
)List every problem#
When the user needs to fix a whole form, one error at a time is tedious. issues checks everything and returns a record per problem, each with a path (object keys and 1-based array positions) and a message. An empty list means the value is valid.
import "std:schema"
auto order = schema.object({
customer: schema.string().minLen(2),
items: schema.array(schema.object({
sku: schema.string(),
qty: schema.integer().min(1)
}))
})
auto bad = {customer: "X", items: [{sku: "a", qty: 2}, {sku: "b", qty: 0}, {qty: 1}]}
for issue in order.issues(bad) (
log issue.path issue.message
)
log order.issues({customer: "Ada", items: []}).lenClean up while you check#
parse returns the value normalised, not just approved. trim removes surrounding whitespace, defaultValue fills in a missing key, and oneOrMany accepts one item or a list and always gives you a list.
import "std:schema"
auto signup = schema.object({
email: schema.string().trim(),
newsletter: schema.boolean().defaultValue(false),
tags: schema.oneOrMany(schema.string())
})
auto a = signup.parse({email: " ada@example.com ", tags: "math"})
log a.email "|" a.newsletter "|" a.tags
auto b = signup.parse({email: "bob@example.com", newsletter: true, tags: ["x", "y"]})
log b.newsletter b.tagsOptional, nullable and strict#
optional allows a missing key (or null), nullable allows null, and strict rejects keys the shape doesn't mention. By default unknown keys pass through, which is kind to newer clients but means you should not trust them. Use strict when you'd rather refuse.
import "std:schema"
auto s = schema.object({id: schema.integer(), note: schema.string().optional()})
log s.isValid({id: 1})
log s.isValid({id: 1, note: null})
log s.isValid({id: 1, extra: true})
log s.strict().isValid({id: 1, extra: true})
log s.strict().parse({id: 1, note: "hi"}).noteTry it: add .strict() to s before the first isValid.
Limits and choices#
Numbers have min, max, gt and lt. Strings, arrays and objects have minLen, maxLen and length. enum and literal accept fixed values, and union takes the first schema that matches.
import "std:schema"
auto level = schema.enum(["low", "mid", "high"])
log level.isValid("mid") level.isValid("max")
auto id = schema.union([schema.integer(), schema.string().minLen(3)])
log id.isValid(5) id.isValid("abc") id.isValid("ab") id.isValid(true)
auto pct = schema.number().min(0).max(100)
log pct.isValid(99.5) pct.isValid(100.1)Try it: add "mid" to the enum values, then check "max".
Maps with record#
record is for objects whose keys you can't list but whose values all look alike, such as a map from name to score.
import "std:schema"
auto scores = schema.record(schema.integer().min(0))
log scores.isValid({ada: 10, bob: 7})
log scores.issues({ada: 10, bob: -3, cy: "x"}).lenCombining schemas#
Schemas are immutable: every method returns a new one. That makes it easy to build variations. extend adds fields, partial makes every field optional (useful for a PATCH request), and requireAny demands at least one of several fields.
import "std:schema"
auto base = schema.object({name: schema.string(), email: schema.string()})
auto admin = base.extend({role: schema.enum(["admin", "owner"])})
auto patch = base.partial().requireAny(["name", "email"])
log base.isValid({name: "A", email: "a@x"})
log admin.isValid({name: "A", email: "a@x"})
log patch.isValid({email: "new@x"})
log patch.isValid({})Validating JSON from outside#
Put the pieces together: parse untrusted text with json, then validate before using it.
import "std:json"
import "std:schema"
auto request = schema.object({
action: schema.enum(["start", "stop"]),
count: schema.integer().min(1).defaultValue(1)
})
for text in [`{"action": "start"}`, `{"action": "fly", "count": 0}`] (
auto doc = json.parse(text).unwrap()
auto problems = request.issues(doc)
if problems.len > 0 (
log "rejected:" problems.map(p -> p.path.join(".") ++ " " ++ p.message)
continue
)
auto req = request.parse(doc)
log req.action req.count
)What is the difference between parse and issues?
Check a signup
Print the path of every problem in the value, one per line, then valid or invalid.
["age"] ["name"] invalid
API reference#
31 members, generated from the std:schema source of OSL 9.3.0.
Functions
schema.any() *schema.Schemaany accepts every value, null included, and returns it unchanged.
schema.array(*schema.Schema item) *schema.Schemaarray accepts arrays whose every item matches item, and returns the items as item normalises them.
schema.boolean() *schema.Schemaboolean accepts true and false.
schema.enum(array values) *schema.Schemaenum accepts any one of values. Throws ValueError for an empty array.
schema.integer() *schema.Schemainteger accepts finite whole numbers.
schema.literal(any value) *schema.Schemaliteral accepts only value itself.
schema.number() *schema.Schemanumber accepts finite numbers.
schema.object(object shape) *schema.Schemaobject accepts objects whose keys match the schemas in shape; a key whose schema isn't optional is required. Keys not in shape pass through unless .strict() is used. Throws TypeError when a shape value is not a schema.
schema.oneOrMany(*schema.Schema item) *schema.SchemaoneOrMany accepts an array of item values or a single item value, and always returns an array.
schema.record(*schema.Schema item) *schema.Schemarecord accepts objects whose every value matches item, whatever the keys.
schema.string() *schema.Schemastring accepts strings.
schema.union(array schemas) *schema.Schemaunion accepts a value matching any of schemas, tried in order, and returns it as the first matching schema normalises it. Throws TypeError when an item is not a schema and ValueError for an empty array.
*schema.Schema
schema.defaultValue(any value) *schema.SchemadefaultValue returns a copy of the schema that uses value in place of null or a missing key.
schema.extend(object shape) *schema.Schemaextend returns a copy of an object schema with the fields of shape added or replaced, keeping its other modifiers. Throws TypeError on other schemas or when a shape value is not a schema.
schema.gt(number value) *schema.Schemagt returns a copy of a number or integer schema that requires values > value. Throws TypeError on other schemas.
schema.isValid(any value) booleanisValid reports whether s.parse would accept value.
schema.issues(any value) {message: string, path: array}[]issues validates value and lists every problem as records {path, message}, where path holds the object keys and 1-based array indexes leading to the bad value ([] for the value itself). [] means the value is valid.
schema.length(number value) *schema.Schemalength returns a copy of a string, array or object schema that requires exactly value characters, items or keys. Throws TypeError on other schemas and ValueError unless value is a whole number >= 0.
schema.lt(number value) *schema.Schemalt returns a copy of a number or integer schema that requires values < value. Throws TypeError on other schemas.
schema.max(number value) *schema.Schemamax returns a copy of a number or integer schema that requires values <= value. Throws TypeError on other schemas.
schema.maxLen(number value) *schema.SchemamaxLen returns a copy of a string, array or object schema that allows at most value characters, items or keys. Throws TypeError on other schemas and ValueError unless value is a whole number >= 0.
schema.min(number value) *schema.Schemamin returns a copy of a number or integer schema that requires values >= value. Throws TypeError on other schemas.
schema.minLen(number value) *schema.SchemaminLen returns a copy of a string, array or object schema that requires at least value characters, items or keys. Throws TypeError on other schemas and ValueError unless value is a whole number >= 0.
schema.nullable() *schema.Schemanullable returns a copy of the schema that also accepts null.
schema.optional() *schema.Schemaoptional returns a copy of the schema that also accepts null or a missing key.
schema.parse(any value) anyparse validates value and returns it normalised: strings trimmed where .trim() is used, defaults filled in, a oneOrMany value as an array, and keys outside a strict object's shape removed. Throws ValueError naming the path of the first problem, such as "schema.Schema.parse: users[2].name: Expected string, received number"; use try(s.parse(value)) for a result, and s.issues(value) to list every problem.
schema.partial() *schema.Schemapartial returns a copy of an object schema with every field optional. Throws TypeError on other schemas.
schema.requireAny(array fields) *schema.SchemarequireAny returns a copy of an object schema that requires at least one of fields to be present, even as null. Throws TypeError on other schemas.
schema.requireAnyValue(array fields) *schema.SchemarequireAnyValue returns a copy of an object schema that requires at least one of fields to hold a non-null value. Throws TypeError on other schemas.
schema.strict() *schema.Schemastrict returns a copy of an object schema that rejects keys not in its shape. Throws TypeError on other schemas.
schema.trim() *schema.Schematrim returns a copy of a string schema that trims surrounding whitespace before checking. Throws TypeError on other schemas.