Data Formats
Geblang includes native parsers and serializers for common data formats.
JSON
Import json:
parse(text)- parse JSON string into Geblang valueparseAs(text, ClassRef)- parse and reconstruct a class instance (calls static__deserialize(dict)when defined, else the constructor matched on parameter names)tryParse(text)- parse without throwing; returns a result dict{"ok": bool, "value": any, "error": any}(on failureokisfalse,valueisnull, anderrordescribes the problem)stringify(value)- serialize to JSON string (accepts dicts, lists, scalars, and user-defined class instances; classes can override with__serialize())write(value, stream)- write JSON to an open writable stream, returning the byte countvalidate(text)- returnsbool(truewhen valid)validateDetailed(text)- returns{"valid": bool, "error": string|null}with detailreader(source)- streaming reader (see below)stream(source, handler)- streaming with a callback
import json;
import io;
let data = json.parse('{"name":"Ada","roles":["admin"]}');
io.println(data["name"]); # Ada
io.println(json.stringify({"ok": true})); # {"ok":true}
let result = json.tryParse("not json");
if (!(result["ok"] as bool)) {
io.println("invalid JSON: ${result["error"]}");
} else {
io.println("parsed: ${result["value"]}");
}
tryParse never throws: it always returns a result dict. Check
result["ok"] before reading result["value"]. A == null guard
would never fire (the dict itself is never null), so failures would
pass silently.
Whole-value JSON parsing, validation and encoding are limited to 512 nesting levels. Cyclic values and dictionary keys other than strings raise a catchable error when serialized; a shared child used in two sibling positions is valid. A failed encode does not affect later calls. Decimal output retains ten fractional places. When exact decimal wire precision matters, serialize a decimal as a string explicitly.
Small unescaped strings extracted from documents larger than 1 MiB own their storage when shorter than 4 KiB, so retaining them does not retain the document. Other string storage choices are an implementation detail.
Writing to a borrowed stream
json.write(value, stream) matches json.stringify formatting, including its
existing ten-fractional-place decimal policy and class serialization hooks. It
emits output incrementally through a reusable 16 KiB buffer rather than building
the complete document string. Large individual strings, bytes values and class
serialization results can still require larger temporary storage.
import json;
import streams;
let output = streams.open("report.json", "w");
try {
let written = json.write({"ok": true, "amount": 1.25}, output);
} finally { output.close(); }
The destination remains borrowed: the helper never calls its flush or close
methods. It supports raw IO handles and the __writeBytes(bytes) or
__write(string) protocol, preferring bytes. Writers return a byte count; short
writes are retried, while zero progress and invalid counts raise errors. A path
string is not a stream. Encoding or IO errors may leave partial output; callers
that need atomic publication should write to a temporary file first.
JSON Lines
Import jsonl for one complete JSON value per physical line. This format is
also called NDJSON. Unlike json.reader, it reads records rather than JSON
syntax events. Newline-delimited records work well for logs and exports.
| Function | Returns | Description |
|---|---|---|
reader(source, opts = {}) |
Reader |
Lazily reads records from text, bytes, or a readable stream. |
readAll(source, opts = {}) |
list<any> |
Reads every record into memory. |
writer(sink) |
Writer |
Writes compact JSON records to a writable stream. |
stringify(values) |
string |
Converts an iterable to newline-terminated JSON Lines text. |
Reader.hasNext() must be checked before next(): JSON null is a valid
record, so null cannot signal end of input. next() raises ValueError at
EOF. A Reader buffers at most one line beyond its current input chunk. The
default maxLineBytes is 1 MiB; set a positive limit in opts if needed.
Empty lines raise ParseError unless skipBlank is true. Errors include the
1-based physical line number. LF and CRLF are accepted, including a final
record without a newline. Invalid UTF-8 is rejected.
import jsonl;
import streams;
import io;
let source = streams.open("events.jsonl", "r");
defer source.close();
let records = jsonl.reader(source);
defer records.close();
while (records.hasNext()) {
let event = records.next();
io.println(event);
}
Pass text content directly for small inputs: jsonl.readAll("1\nnull\n").
To read a path, open it explicitly with streams.open(path); string arguments
are interpreted as content. The writer retries partial sink writes and emits
one \n after each value. Reader.close() and Writer.close() do not close
caller-provided streams. Writer.close() flushes its sink. The caller owns and
closes that sink.
For converting a CSV reader one row at a time:
import csv;
import jsonl;
import streams;
let input = streams.open("events.csv", "r");
let output = streams.open("events.jsonl", "w");
defer input.close();
defer output.close();
let rows = csv.reader(input);
let records = jsonl.writer(output);
defer rows.close();
defer records.close();
while (rows.hasNext()) {
records.write(rows.next());
}
YAML
Import yaml:
parse(text),parseAs(text, ClassRef),tryParse(text)(returns a{"ok", "value", "error"}result dict; see JSON above),stringify(value)validate(text)- returnsbool;validateDetailed(text)- returns detail dictreader(source),stream(source, handler)
import yaml;
import io;
let cfg = yaml.parse("server:\n port: 8080\n");
io.println(cfg["server"]["port"] as string); # 8080
TOML
Import toml:
parse(text),parseAs(text, ClassRef),tryParse(text)(returns a{"ok", "value", "error"}result dict; see JSON above),stringify(value)validate(text)- returnsbool;validateDetailed(text)- returns detail dict
TOML is typically configuration-sized and does not expose a streaming reader.
import toml;
import io;
let cfg = toml.parse('[database]\nurl = "postgres://localhost/app"\n');
io.println(cfg["database"]["url"]);
XML
Import xml:
parse(text),parseAs(text, ClassRef),tryParse(text)(returns a{"ok", "value", "error"}result dict; see JSON above),stringify(value)validate(text)- returnsbool;validateDetailed(text)- returns detail dictreader(source),stream(source, handler)
parse returns an element dict (name, attributes, children, text),
and stringify accepts one back.
Classes and XML (1.41.3)
xml.parseAs(text, ClassRef) fills a class from an element:
- A child element or an attribute with a field's name fills that field; when both exist, the child element wins. Unknown elements are ignored, and the root element's name does not matter.
- Text converts to the field's declared type:
int,decimal,float,bool(true/false/1/0) orstring. Text that does not fit throwsTypeErrornaming the field. An empty element fills a nullable non-string field withnull. - A child element fills a class-typed field, recursively. Repeated child
elements fill a
list<T>,set<T>orT[]field, in document order. - Inherited fields are filled like the class's own.
xml.stringify(instance) writes the reverse shape: <ClassName> with one
child element per public field, sorted by name, repeating the element for
list items and omitting null fields. A class with __serialize() writes
the returned value instead.
import io;
import xml;
class Address {
string city;
}
class Person {
string id;
int age;
list<string> tags;
Address home;
}
let p = xml.parseAs("<person id=\"p1\"><age>36</age><tags>a</tags><tags>b</tags><home><city>Leeds</city></home></person>", Person);
io.println("${p.id} ${p.age} ${p.tags} ${p.home.city}"); # p1 36 ["a", "b"] Leeds
io.println(xml.stringify(p));
# <Person><age>36</age><home><city>Leeds</city></home><id>p1</id><tags>a</tags><tags>b</tags></Person>
HTML (1.28.0)
For parsing real-world HTML with the lenient HTML5 parser and querying it with
CSS selectors, use the html module. It has its own chapter: see
HTML.
Class instances: stringify and parseAs
json.stringify, yaml.stringify, and toml.stringify accept
user-defined class instances. By default the call emits the
public fields - any field whose name does not start with _ or
__. Classes can replace the default by implementing
__serialize(); the return value is recursively serialised. Inherited
fields are included, and string values are written as-is: <, > and &
are not escaped, for instances exactly as for dicts and lists (1.41.3).
import json;
import io;
class Point {
int x;
int y;
int _secret;
func Point(int x, int y) { this.x = x; this.y = y; this._secret = 99; }
}
io.println(json.stringify(Point(3, 4)));
/* {"x":3,"y":4} - _secret is omitted. */
The companion parseAs(text, ClassRef) reconstructs an instance.
It calls a static __deserialize(dict) factory on the target
class when one is defined; otherwise it matches dict keys against
the constructor's parameter names and calls the constructor
positionally.
let p = json.parseAs("{\"x\":3,\"y\":4}", Point);
io.println(p.x);
Reconstruction is recursive: a field whose declared type is another
class (or a list / dict of one) is itself deserialized into an
instance, so a whole object tree comes back fully typed.
class Address { string postcode; }
class Order {
string customer;
Address shipTo; # nested object
list<Address> stops; # list of objects
}
let o = json.parseAs(
"{\"customer\":\"Ada\",\"shipTo\":{\"postcode\":\"EC1\"},\"stops\":[{\"postcode\":\"W1\"}]}",
Order);
io.println(o.shipTo.postcode); # EC1
io.println(o.stops[0].postcode); # W1
Fields typed any keep their raw parsed value, and a class with
__deserialize controls its own nested handling. JSON, YAML, and TOML do
not distinguish float from decimal, so a parsed number converts to the
numeric type of its field or constructor parameter: 10 fills a decimal
or float, 1.5 fills a float, 3.0 fills an int, and elements of
list<float> or dict<string, decimal> convert the same way. Any other
mismatch (a string where an int or an object is declared, or 2.5 for an
int) throws TypeError naming the field, such as
cannot assign string to int field Plain.age.
A subclass is filled from its own fields and every inherited field (1.41.3),
including ancestors declared in other modules; each inherited field keeps its
declared type, so the same conversions and TypeErrors apply.
The same conventions apply to yaml.parseAs, toml.parseAs, and
xml.parseAs. See chapter 6's Serialisation section for details.
CSV
Import csv for both in-memory parsing and streaming.
| Function | Returns | Description |
|---|---|---|
csv.parse(text, options?) |
list<list<string>> |
Parses CSV text into a list of rows; each row is a list of cell strings. Options: delimiter (single char), trimSpace (bool). |
csv.parseDict(text, options?) |
list<dict<string, string>> |
Parses CSV text using the first row as headers; returns a list of dicts keyed by header name. Same options. |
csv.stringify(rows, options?) |
string |
Serialises a list-of-lists OR a list-of-dicts into CSV text. For a list of dicts, the first dict's keys become the header row; missing keys in later dicts produce empty cells. Options: delimiter. |
csv.reader(source) |
CsvReader |
Returns an incremental pull reader over a file / bytes / string source. Use .hasNext(), .next(), .close(). |
csv.stream(source, handler) |
void |
Push-based streaming with one callback per row. |
import csv;
import io;
# In-memory parse / stringify.
let rows = csv.parse("name,age\nAda,37\nGrace,55");
io.println(rows[1][0]); # Ada
let dicts = csv.parseDict("name,age\nAda,37\nGrace,55");
io.println(dicts[0]["age"]); # 37
# Custom delimiter + trimSpace.
let semi = csv.parse("a; b; c\n1; 2; 3", {"delimiter": ";", "trimSpace": true});
# Large file - streaming.
csv.stream(io.open("large.csv", "r"), func(list<string> row): void {
io.println(row[0]);
});
MessagePack (1.6.0)
The msgpack module encodes Geblang values to MessagePack 5 bytes
and back. The codec is hand-rolled (no Go dependency) and covers
the common cases: nil, bool, signed integers, float, str, bin,
array, and map. Ext types and the timestamp extension are not
supported in 1.6.0.
import msgpack;
let bytes = msgpack.encode({"items": [1, 2, 3], "ok": true});
let value = msgpack.decode(bytes);
Functions
| Function | Returns | Description |
|---|---|---|
msgpack.encode(value) |
bytes |
Serialises any encodable value. Throws on unsupported types or int values outside int64 range. |
msgpack.decode(bytes) |
any |
Parses bytes into a Geblang value. Throws on malformed input or trailing bytes. |
msgpack.tryDecode(bytes) |
?any |
Like decode but returns null instead of throwing on malformed input. |
msgpack.validate(bytes) |
bool |
True when bytes is exactly one well-formed MessagePack value. |
Type mapping
| Geblang | MessagePack |
|---|---|
null |
nil (0xc0) |
bool |
false / true (0xc2 / 0xc3) |
int (in int64 range) |
int family (positive / negative fixint, int 8/16/32/64) |
float |
float 64 (0xcb) |
decimal |
str (encoded as the canonical decimal string for lossless round-trip) |
string |
str family (fixstr, str 8/16/32) |
bytes |
bin family (bin 8/16/32) |
list<T> |
array family (fixarray, array 16/32) |
dict<K, V> |
map family (fixmap, map 16/32) |
decimal values round-trip as MessagePack strings because the
spec has no decimal type. Round-tripping 1.50 yields the string
"1.5000000000"; cast with (value as string) as decimal to
recover the typed Decimal.
Limitations
- Ext types are not encoded or decoded. Decoding an ext-tagged value raises an error.
- The timestamp extension (ext type -1) is not recognised.
intvalues whose magnitude exceeds 64 bits raise on encode.
Streaming readers
json.reader, yaml.reader, and xml.reader return a token/event stream
for large or unbounded documents. Each call to next() returns an event dict
{"type": string, "value": any} or null at EOF. The reader also exposes
hasNext() and close().
Event types for JSON: startObject, endObject, startArray, endArray,
key, value, error. Structure events (startObject etc.) have null
as their value; key carries a string; value carries the parsed scalar.
import json;
import io;
let r = json.reader("[1, 2, 3]");
let ev = r.next();
while (ev != null) {
io.println(ev["type"] as string);
ev = r.next();
}
r.close();
# startArray
# value
# value
# value
# endArray
Pull-style with hasNext():
import json;
import io;
let r = json.reader('{"a": 1}');
while (r.hasNext()) {
let ev = r.next();
io.println(ev["type"] as string);
}
r.close();
The reader accepts a string, bytes, or an open IOStream (io.open(...))
as its source.
Serde - format-agnostic serialization
Import serde when the format is runtime-determined (for example, a tool
that accepts a --format json|yaml|toml flag):
import serde;
let value = serde.parse("json", '{"ok":true}');
let text = serde.stringify("yaml", value);
Supported format strings: "json", "yaml", "toml".
Schema validation
Import schema for JSON Schema-style validation of dicts and lists. This is
useful for validating API request bodies, configuration files, and parsed
data before using it.
import schema;
import io;
let userSchema = {
"type": "object",
"required": ["name", "email"],
"properties": {
"name": {"type": "string"},
"email": {"type": "string"},
"age": {"type": "number", "minimum": 0, "maximum": 150},
"roles": {
"type": "array",
"items": {"type": "string"}
}
}
};
let result = schema.validate({"name": "Ada", "email": "[email protected]"}, userSchema);
if (result["valid"] as bool) {
io.println("valid");
} else {
for (err in result["errors"]) {
io.println(err);
}
}
schema.validate(value, schema) returns:
{"valid": true, "errors": []}on success (theerrorslist is always present and empty when valid){"valid": false, "errors": list<string>}on failure, with one message per violation
Common schema keywords
| Keyword | Applies to | Description |
|---|---|---|
type |
any | "string", "number", "bool", "object", "array", "null" |
required |
object | list of required property names |
properties |
object | map of property name to sub-schema |
items |
array | sub-schema applied to each element |
minimum |
number | inclusive lower bound |
maximum |
number | inclusive upper bound |
minLength |
string | minimum character count |
maxLength |
string | maximum character count |
minItems |
array | minimum element count |
maxItems |
array | maximum element count |
enum |
any | value must be one of the listed values |
nullable |
any | allow null in addition to the declared type |
Pair schema.validate with json.parse or yaml.parse to validate
untrusted external input:
let body = json.parse(io.stdinReadAll());
let check = schema.validate(body, requestSchema);
if (!(check["valid"] as bool)) {
throw errors.new("ValidationError", "invalid request body");
}