Skip to content

Errors and Validation ​

protoarray throws three error classes, all TypeErrors with a code and the name of the schema involved (schema, '' for a nameless one), so a log line says which contract was broken.

ClassWhenProperties
SchemaDefinitionErrora snapshot protoarray cannot read, or wrong options, when the schema is builtcode, schema (the name), field (address.city)
EncodeErrorencode gets a value with no wire: the wrong shape, a cycle, a union value outside the unioncode, path, schema
DecodeErrorthe wire does not match the schema, or parse gets text that is not JSONcode, path, schema, cause, id (an envelope's)

A definition is metaschema's to read, so a definition metaschema refuses throws metaschema's own SchemaDefinitionError (ERR_UNKNOWN_TYPE, ERR_INVALID_DEFINITION, …): another class with the same code, schema and field, which instanceof protoarray's class does not match. Tell the two by name and code, or catch both.

Decode errors ​

CodeMeaningExample message
structurenot an array where one is expected, a wrong tuple, flat or map length, a bad sparse mask, an interned payload that is not [table, body], an envelope that is not [id, payload], a payload nested deeper than the call stack (or than maxDepth levels of a recursion), a streamed line or item longer than maxLengthField "address" is not an array, Field "next.next…" is nested too deeply, Field "[3]" is longer than maxLength allows
requireda required field is null or missingField "email" is required
typea value of the wrong type, or a null element where the item is requiredField "orders[1].qty" is not of expected type: number
enuman index outside the enum (a long enum names its first 16 values)Field "role" is not of enum: admin, editor
uniona union's tag is not one of its indexes (always checked)Field "type" is not of union: call, event
extraelements beyond the schema, with { strict: true }Value has extra elements
jsonparse got text that is not JSON (an NDJSON line, an item of a streamed array); cause is the SyntaxErrorValue is not valid JSON
schemaan envelope's id is not in the registry, or names a version the reader cannot resolve (cause is its ERR_INCOMPATIBLE); the id is error.idValue is not of a schema this reader can read

path names the slot: '' for the value itself, keys joined with dots, indexes in brackets: orders[1].qty, [2].name in a collection, [].price for a column of a columns collection. A key that holds a dot, brackets or braces, begins with a quote or is empty is a quoted JSON string in brackets (["a.b"].c), and so is a dictionary's key that is not a plain name, cut to 64 characters (scores["a b"]): the payload gives it. A path keeps 64 levels at most, both ends. Paths cost nothing until an error is thrown. message, path and code never carry a line break or an escape sequence from the payload, so they are safe to log; cause may quote it.

js
try {
  User.decode([1, 'Alex', 7]);
} catch (error) {
  error instanceof DecodeError; // true
  error.code;                   // 'enum'
  error.path;                   // 'role'
  error.message;                // 'Field "role" is not of enum: admin, editor, viewer'
}

Encode errors ​

encode trusts its values and throws only for what has no wire at all:

CodeMeaningExample message
object, array, set, mapa record that is not an object, a collection or tuple that is not an array, a set that is neither a Set nor an array, a map that is not a Map (told by its brand, not a tag it claims)Field "orders[1]" is not an object
iterableencodeEach and the stream helpers got something that is not iterableValue is not iterable
deptha cyclic value, or one nested deeper than the call stack (or than maxDepth levels of a recursion)Field "next.next…" is nested too deeply
uniona union's discriminator is not one of its valuesField "type" is not of union: call, event
typea value a delta list or column cannot hold (only integers have exact differences), a decimals number that scales past the safe integersField "ids" is not of expected type: [integer]

What is validated ​

The two sides of a codec are checked by the two libraries:

SideChecked byHow
The values given to encodemetaschemaschema.source.check(value), or parse
The wire given to decodeprotoarrayvalidate: true, the default

Decoding with validation checks the array structure, the type of every scalar, required fields, enum ranges, tuple lengths and flat/columns shapes. It costs a few percent of the decode time, and it is the only error path: whatever the input, decode and parse throw DecodeError and nothing else. { validate: false } trusts the wire: it skips the type and required checks, but still checks the array structure, a sparse mask and a union tag, so a bad payload fails with structure or union rather than an accidental TypeError from inside the decoder. A value that has no wire form of its type (a non-digit string in a bigint slot, a string in an enum slot) is then left as it is: not converted, not rejected.

Encoding trusts the values, as protoarray 1.0 did: an unknown enum value travels as null, a value of the wrong type travels as it is, a missing required field as null. Keys the schema does not have and tuple elements past its length are ignored: a schema is a projection. The values are metaschema's to check, against every rule of the definition (types, required fields, enums, and the rules the wire knows nothing of: length, min, pattern, format, validate). A schema built from a metaschema Schema keeps it as source:

js
const result = User.source.check(input);
if (!result.valid) reply(400, { errors: result.errors });
else send(User.stringify(input));

parse does the same and applies metaschema's defaults and field codecs on the way (see metaschema's parse). A schema built from a snapshot has no source; build the metaschema schema where the values are produced, from the same definition. A decoded object passes metaschema's check: every key present, null for the optional ones that were missing.

Depth ​

A recursive schema (Node with kids: { array: 'Node' }, a linked list) goes as deep as the call stack by default: past it, decode throws DecodeError structure and encodeEncodeError depth, Value is nested too deeply, with no path. Where that happens depends on the backend and the engine: the closures backend uses more frames a level, so it reaches the end of the stack sooner than generated code (on Node.js 24, about 1,800 levels of a linked list against 3,500, and less inside a deep call). A server on generated code can then write a payload that a client under a strict CSP cannot read.

maxDepth sets the limit instead: every entity a recursion goes through counts its levels, and one past the limit is the same error, with its path, on either backend and in generated modules.

js
const { Node } = Schema.fromModel(model, { maxDepth: 512 });
Node.decode(deeper); // DecodeError: Field "kids[0].kids[0]…" is nested too deeply

512 is a depth both backends reach on every engine. A schema without a recursion is never counted; one with a limit encodes about 8% and decodes about 3.5% slower on deep trees, which is why the limit is opt-in. The entities a reference reaches, schema.list and a resolved schema (the reader's option) take it; entities gives one entity a limit of its own.

Schema errors ​

A broken snapshot or option throws protoarray's SchemaDefinitionError when the schema is built. Messages say where: Unknown type "float" at "price".

CodeCause
ERR_INVALID_SNAPSHOTnot a snapshot of format 1, no entity of the name asked for, an entity that is not an object, a field without a type, a schema nested too deeply, a collection without its key or element, a tuple without elements, decimals outside 0–15, aliases that are not a list of keys, references that lead back to themselves with no record, a snapshot that breaks the reader unchecked (cause)
ERR_UNKNOWN_TYPEan unknown type, a custom type with a check of its own and no wire type in types
ERR_INVALID_ENUM, ERR_INVALID_FLAGSan empty list, duplicates, wrong value types, __proto__ (neither has a size limit)
ERR_INVALID_UNIONa union without a discriminator, an empty union, a branch that is not a struct with the discriminator as a required enum without a default, the same value in two branches
ERR_UNKNOWN_REFERENCEa reference to an entity the snapshot does not have (a reference by name needs a Model, or the schema's own name)
ERR_RESERVED_KEY__proto__ as a key
ERR_INTEGER_KEYa key or a flag name like '1', which JavaScript would order first
ERR_INVALID_DEFINITIONa struct without fields, an empty key
ERR_INVALID_OPTIONSan unknown option, a wrong type, a path that names no place of the schema or a place of another kind (sparse on a non-struct, a layout on anything but a collection of records, delta on a string, intern on anything but a string), layout: 'rows' on anything but a collection, sparse on a union's branch, an option of one schema given to Schema.fromModel instead of entities, an entities key that names no entity of the snapshot or an entity's own entity, entities or metaschema, a snapshot of several entities without entity, entity other than a metaschema Schema's own, a fingerprint that gives no non-empty string, an aliases path of resolve that names no field of the reader, a maxLength that is not a positive integer or Infinity
ERR_SCHEMA_EXPECTEDSchema.from got neither a metaschema Schema or Model, a snapshot, a protoarray schema nor a definition with { metaschema }; encode(schema, …) and friends got something that is not a Schema
ERR_CODEGEN_UNAVAILABLE{ codegen: true } where new Function is blocked
ERR_COMPILED_MISMATCHa generated module's codec is of another schema than the snapshot given with it, or the module was generated by another version of protoarray
ERR_INCOMPATIBLEresolve cannot read the writer as the reader: a narrowing type change, a required reader field the writer lacks, a union split between branches, a chain of references too long for the stack
ERR_ID_COLLISIONa registry got two schemas of different signatures with one id: the fingerprint is too narrow for it
ERR_UNKNOWN_SCHEMAa registry's encode got as an id it does not know

Released under the MIT License.