Documentation/Chapters

JSON, schemas and serialization

On this page

Serialization converts application values into a transport representation and back. Prefer typed conversion when you know the data shape; use a dynamic JSON tree when the structure is determined at runtime. In either case, invalid input is a checked error rather than a partly initialized record.

Decode a typed record

TYPE Customer
    Id AS INTEGER
    Name AS STRING = "Unknown" JSON NAME "full_name"
    Nick AS OPTIONAL OF STRING = NONE JSON OMITNONE
END TYPE
customer = DecodeJSON("{""Id"":42}") AS Customer
PRINT customer.Name
PRINT EncodeJSON(customer)

The AS annotation supplies the requested result type. A known typed destination can supply the same context. Missing fields use explicit defaults, then optional NONE, otherwise fail. Invalid supplied values never silently select a default. OMITNONE leaves an absent optional out of encoded objects.

INTEGER and unsigned values retain exact integer representation. DOUBLE must be finite for standard JSON. Lists/sets become arrays, records become objects, enums use their integers and choices use exactly one named alternative. Nonzero-based or multidimensional arrays include checked bounds metadata. Resource handles have no serializable standard representation.

Work with dynamic values

request = JsonObject([
    JsonField("name", "Ada"),
    JsonField("active", TRUE)
])
JsonPut(BYREF request, "visits", 3)
PRINT StringifyJSON(request)

JsonValue is a closed data representation, not a universal dynamically typed BASIL variable. ParseJSON parses text into that tree; StringifyJSON writes it. Typed ToJSON/FromJSON bridge between the tree and known BASIL types. JsonObject and JsonArray construct independent values. JsonPut replaces/inserts one object member and preserves previous independent copies. Wrong-kind targets fail with JsonError.

Choose wire directions and names

SERIALIZE supplies shared mapping rules; JSON clauses override the corresponding shared property for JSON. NAME changes wire spelling. READONLY means encode-only, WRITEONLY means decode-only, and EXCLUDE removes both directions; these are wire rules, not permission to mutate BASIL fields. Decoding must still produce a complete valid value. Unknown/disallowed fields fail unless the supplied options permit ignoring them. Duplicate keys/mapped names remain errors.

TYPE Customer
    Name AS STRING SERIALIZE NAME "full_name" JSON NAME "name"
END TYPE
customer = Customer { Name="Ada" }
PRINT StringifyJSON(ToValue(customer))
PRINT EncodeJSON(customer)

ToValue/FromValue follow shared rules; EncodeJSON/DecodeJSON follow effective JSON rules. This lets one application schema serve multiple codecs without making JSON the meaning of every format’s names.

Customize conversion

JsonCodec profiles hold independent per-type converters. Field and TYPE defaults can declare ENCODE WITH and DECODE WITH. Converter representation types must agree. Selection is per direction: JSON field, shared field, supplied profile, JSON type, shared type, standard handling. Representation roots bypass their selected rule once to avoid immediate self-recursion; child values use normal selection.

A converter may accept a second read-only JsonLocation or SerializationLocation to retain path/offset context in a custom error. Its original declared failures propagate through adapters and compiled libraries. Registration currently uses named nongeneric noncapturing functions. Broad mutable global formatter registries and arbitrary resource serialization are not provided.

Attach format-specific metadata

TYPE FieldOptions
    Number AS INTEGER
END TYPE
TYPE Message
    Id AS INTEGER FORMAT FieldOptions { Number=1 }
END TYPE
DIM witness AS OPTIONAL OF Message = NONE
options = FormatMetadata(witness, "Id") AS OPTIONAL OF FieldOptions
IF LET field = options THEN
    PRINT field.Number
END IF

FORMAT attaches a typed constant record to a type or field. A codec interprets its own options; merely naming an options type does not install a parser. SerializationSchema describes shared mappings; FormatMetadata returns independent typed optional metadata without constructing an uninitialized instance. Arbitrary build-time functions do not execute in these annotations.

Inspect declared types without constructing sample values

TypeSchema, SerializationTypeSchema and JsonTypeSchema return a finite graph of declared types. Use an OPTIONAL OF T = NONE witness even when T has required fields or owns resources. The query examines declarations, not a sample object’s current contents, and acquires no application resource.

TYPE Node
    Name AS STRING
    Children AS LIST OF Node = []
END TYPE
DIM witness AS OPTIONAL OF Node = NONE
graph = JsonTypeSchema(witness)
root = graph.Types[graph.Root]
PRINT root.Name
FOR EACH field IN root.Fields
    PRINT field.Name; ":"; graph.Types[field.DeclaredType].Name
NEXT
childrenType = graph.Types[root.Fields[1].DeclaredType]
PRINT childrenType.ElementType = graph.Root

The final comparison is TRUE: the list element refers back to the existing Node descriptor, rather than expanding an infinite tree. Node IDs index this graph only; -1 means no reference. Repeated types share nodes. Generic arguments, including parameters absent from fields, have their own TypeArguments references. Enum members, choice alternatives and collection element/key/value references describe their respective shapes.

Choose the view deliberately:

Query Information
TypeSchema Physical BASIL fields and explicit-default presence
SerializationTypeSchema Shared names, access rules and declared converter shapes
JsonTypeSchema Effective declaration-default JSON mappings

Each TypeDescriptor has a display Name, an opaque Identity, Kind, ownership/privacy flags and shape-specific facts. Compare identities as whole values within their documented scope; do not parse them or persist them as versioned schema IDs. TypeFieldInfo distinguishes physical Name, WireName, DeclaredType and each converter direction. HasDefault means an explicit field default, not every fallback that a decoder could supply.

Graph fields are read-only public data, and copied descriptor lists or FORMAT options are independent values. FormatMetadata(graph, node[, field]) retrieves typed options from the selected declaration. Opaque types expose no fields or field annotations through this query, including otherwise readable PUBLIC fields. Resources, functions, pointers and dynamic JSON explicitly lack a standard static wire shape; a query does not authorize serialization of them.

WireSupported describes a node’s standard handling before its root converter; it is not proof that every reachable child is serializable. A wire traversal must select the direction’s converter, bypass that representation’s root converter once, then visit its children normally. Track direction and bypass state as well as node ID to handle normalization and recursion correctly. Per-instance JsonCodec profiles are not inputs to these declaration-default queries. JSON Schema generation, validation and data synthesis remain separate library work. See the type-metadata reference for full descriptor fields, wire labels, converters and privacy rules.

Embed checked data while building

EMBED reads an input while compiling and creates a read-only module value. The resulting executable does not need the input file or decoder at runtime. Use it for default configuration, templates or other data shipped with the application.

Save this file as defaults.json beside the source:

{"Host":"localhost"}
TYPE ServerConfig
    Host AS STRING
    Port AS INTEGER = 8080
END TYPE
EMBED Defaults FROM "defaults.json" AS ServerConfig USING Json
local = Defaults
local.Port = 9000
PRINT Defaults.Host; ":"; Defaults.Port
PRINT local.Port

This prints localhost:8080 then 9000. Defaults and its nested contents cannot be assigned, transferred or passed BYREF. A normal assignment or BYVAL use produces an independent owned value; read-only calls borrow. PUBLIC/PRIVATE work as for other module declarations, and imported inputs resolve relative to their own source.

Without USING, the supported raw form is STRING. It preserves exact UTF-8 input bytes, including BOM, embedded NUL and line endings:

Hello from embedded text!
EMBED Message FROM "message.txt" AS STRING
PRINT Message;

The fixture includes a final newline, which the trailing semicolon preserves without adding another. Raw text embeddings can also serve ordinary constant/default contexts. Values produced by a decoder do not become general constant-expression factories.

USING can name an ordinary visible decoder; no registration or plugin is required:

8081
EMBED Port FROM "port.txt" AS INTEGER USING ReadPort
PRINT Port
FUNCTION ReadPort(text AS STRING) AS INTEGER FAILS NumericError
    RETURN VAL(TRIM(text))
END FUNCTION

The first decoder parameter receives STRING content; trailing parameters use their checked defaults and AS supplies result context. The same decoder works at runtime. Json is shorthand for DecodeJson when no visible function named Json overrides it. Decoders can depend on other embedded values; cycles are rejected. Main and application top-level statements do not execute during decoding.

Decoded data can recursively contain records, collections, arrays, optionals, choices and enums. Resources, pointers, function values, task/channel handles and errors as successful data are rejected. Decoders may return their declared errors. The checked decoder and its helpers must avoid I/O, clocks, randomness, task starts, unsafe operations and unknown foreign behavior. The build also isolates execution and enforces bounded input, memory, time and output. A failing decoder is a build diagnostic with the source/input location and original error, not a recoverable failure in the resulting application.

Paths must stay inside the declaring project tree, or the standalone source’s directory. Absolute paths, escaping symlinks and unstable/nonregular inputs are rejected. All declarations are checked, including unused ones; unreachable embedded data and decoding machinery are omitted from the executable. Compatible libraries retain public/private embedded values when moved without producer sources/data.

Successful build data may be cached under build/.basic/embed beneath the entry source directory. Changed content or decoder inputs invalidate the relevant work; semantic checks remain active. Failures are not cached. Delete that directory to clear the cache; a read-only project can build without it. See the embedding reference for exact limits and failure policies.

The book verifier removes these three fixture input files before executing their compiled programs, checking that the programs no longer depend on those files.

ReadJSON/WriteJSON, JSON Lines and typed HTTP helpers extend the same checked mapping model to files and network bodies. The JSON reference includes exact names, limits and error contracts. YAML, general TOML and XML codecs remain unimplemented; EMBED does not install those parsers. The project manifest’s TOML subset is a separate facility.

From the BASIL documentation. View source ↗

Find your way around BASIL

Type to search the manual.