Jeto
Overview
Jeto is xeto-typed JSON: the spec supplies the types, so the values don't have to declare them. It is the representation of Xeto instances in the widely used data interchange format JSON.
Jeto reads as ordinary JSON. A date is the string "2024-11-26" and a dict is
a plain object, so a consumer which does not know Xeto still sees data it can
work with.
Jeto is represented by the file spec JetoFile.
Jeto is distinct from Hayson, the Haystack 4 JSON encoding, which is self-typed JSON: each value declares its own kind. The two are different formats with different goals. Hayson makes every value self-describing at the cost of verbosity, and needs no schema to read back. Hayson only supports the original fixed scalar types, while Jeto supports the full Xeto type system. Jeto stays terse by leaning on the spec, and boxes only the values a spec does not cover.
Scalars
JSON supports a very limited type system, which requires significant type erasure. Jeto opts for a clean, simple mapping where most scalars are mapped to a string (versus mapping scalars to JSON objects). The following mapping is used:
| Xeto | JSON |
|---|---|
| sys::Bool | boolean |
| sys::Int | number |
| sys::Float | number |
| sys::Number | number (if no unit) |
| sys::Number | string (if there is a unit or NaN/-INF/INF) |
| sys::Buf | string (base64url, unpadded) |
| sys::Scalar | string (all other scalars encode as string) |
The singleton scalars encode as their own string forms: Marker as "✓", None
as "∅" and NA as "NA". A JSON null never means NA.
Type Resolution
A JSON string carries no type information, so a decoder must determine the
intended Xeto type from context. Types are never inferred from the content of
a string. In a position with no type, "2024-11-26" decodes as a Str, not a
Date, and "✓" decodes as a Str, not a Marker. Tag names carry no meaning
either: id decodes as a Ref only where the spec declares one.
Resolution has two halves: the type a position expects, and what the encoded value does with it.
A position's expected type is the first of these which applies:
- For a grid cell, the column's
of - The type declared for this member by the containing dict's spec. That spec
comes from the dict's own
specproperty, from a grid's default row spec, or from the type the enclosing spec declares for the dict itself - None, which leaves the position untyped
Three JSON forms carry their own type and keep it whatever the position expects:
| JSON | Decodes as |
|---|---|
| boxed scalar | the type named by its spec |
| true / false | Bool |
| number | Int with no fraction or exponent, otherwise Float |
Such a disagreement is not an error. A boxed Date in a slot declared Number decodes as a Date. A number in a Bool column decodes as an Int.
Numbers have one exception. Int, Float and Number are interchangeable, so where the expected type is one of the three the number takes it. Against any other type it keeps its lexical form.
A JSON string has no type of its own. It takes the expected type, and decodes as a Str where there is none.
A unitless Number does not survive a position with no type. It encodes as a bare JSON number and decodes as an Int or Float. It must be boxed, or its container must declare a type.
Boxed Scalars
Any scalar may instead be encoded as a JSON object which names its own type:
{"val":"72°F", "spec":"sys::Number"}
The val property is always a JSON string, even for types which have a native
JSON form. A boxed scalar requires no context and is legal anywhere a scalar
may appear: slot value, list element, grid cell or meta tag.
An object whose spec resolves to a non-scalar type is an ordinary dict, not
a boxed scalar.
A boxed Ref may also carry its display string:
{"val":"xyz-123", "spec":"sys::Ref", "dis":"Carytown"}
Encoders support three boxing modes:
| Mode | Behavior |
|---|---|
| none | never box |
| auto | box only where the plain form would not decode back |
| all | box every scalar |
The default is auto. The none mode is lossy in any position with no type: markers, refs, dates and unitless numbers all decode as Str. The auto mode is lossless.
The reserved structural properties are never boxed in any mode: a dict's
spec, a column's name and of, and a grid's spec and of.
Dicts
Dicts are mapped as JSON objects. A dict with a known type includes a spec
property with the type qname. For example, for a Dict conforming to the Xeto
spec foo::Bar:
{
"a":123,
"b":"xyz",
"spec":"foo::Bar"
}
Dicts with no known type omit the spec property. The spec property is
reserved: it always holds a type qname and is never one of the dict's own
tags.
A JSON null means the tag is absent. The object {"a":null} and an object
with no a entry decode to the same dict.
Lists
Lists are mapped as JSON arrays:
["a", "b", "c"]
Null elements are retained.
Grids
Each Jeto grid consists of a JSON object with the following entries:
- A
specentry which identifies them as a Grid, or subclass thereof. - A
metaentry which contains the metadata for the grid itself. - A
colsentry which contains the name, type and metadata for each column. - A
rowsentry which contains each row in Dict format.
For example:
{
"spec":"sys::Grid",
"meta":{
"foo":"quux"
},
"cols":[
{
"name":"a"
},
{
"name":"b",
"meta":{
"dis":"B"
}
}
],
"rows":[
{
"a":0,
"b":"x"
},
{
"a":1,
"b":"y"
}
]
}
Grids may declare types for their cells in three places:
- A column may declare an
ofentry beside itsname, which gives a type to that column's cells. - A grid may declare an
ofentry beside itsspec, which gives a default spec to every row. - A row may include a
specproperty, which overrides the default row spec for that row. The rows of a grid need not all be the same type.
Both of entries sit beside the thing they describe rather than inside its
meta, because they are structural rather than domain tags. meta holds
only domain tags, at both the grid and the column level.
For example:
{
"spec":"sys::Grid",
"of":"ph::Equip",
"cols":[
{
"name":"ts",
"of":"sys::DateTime"
},
{
"name":"v0",
"of":"sys::Number"
}
],
"rows":[
{
"ts":"2026-08-03T09:00:00-04:00 New_York",
"v0":"72.5"
},
{
"spec":"ph::Vav",
"ts":"2026-08-03T10:00:00-04:00 New_York",
"v0":73
}
]
}
A grid whose spec names a Grid subclass takes its default row spec from that
subclass. The grid of entry is for grids whose row spec is not named by a
subclass.
Encoders carry these entries but do not synthesize them. A producer which
relies on column typing declares of itself; otherwise the cells which need
it are boxed.
Specs
Xeto specs are represented in JSON via the JSON Schema specification, using the 2020-12 dialect. This allows for Jeto instances to be validated against Xeto specs without resorting to Xeto-specific tools.
A generated schema types each scalar as a string, so it describes unboxed instances. A boxed scalar is a JSON object where the schema expects a string and will not validate. Instances intended for schema validation are encoded with boxing off.
Consider the following Xeto specs:
Order : Entity {
order
customerName: Str
orderDate: DateTime
orderType: OrderType
items: List <of:Product>
}
OrderType: Enum {
kitchen
bathRoom
livingRoom
secretLab
}
Product : Dict {
product
name: Str
price: Float
}
The JSON Schema representation of these specs is as follows.
- All of the definitions are stored in the
$defssection of the schema, keyed by flat qname such assys.Ref. There is no version stamp: a single document is one coherent namespace and cannot hold two versions of a library. Str,Bool, andIntare stamped inline where they are used. Every other type gets a$defsentry and is referenced with$ref.- Scalars are represented by a
stringtype with an anchoredpatternregex. Some also carry aformatannotation alongside the pattern, and single-valued scalars such asMarkeruseconstinstead. NumberandFloatare unions, because their wire form is not one JSON type: a bare JSON number, the quoted specials"NaN","INF", and"-INF", and forNumbera quoted string when the value carries a unit.- Enums (a special case of Scalars) are represented by a
stringtype with anenumarray. - Lists are represented by an
arraytype with a reference to the type of the array's elements. - Dicts are represented by an
objecttype which defines the dict's slots as a set ofpropertiesentries. If the spec is a subclass of Dict, then theallOfkeyword is used to provide a reference to the parent class. - A maybe slot is represented by omitting it from
required. Null is not used: in a Dict an absent tag and a null tag are the same thing. - Xeto type information which JSON Schema cannot express is attached as an
x-xetoannotation carrying the native::qname and the spec's own meta. Unknown keywords are annotations rather than errors in 2020-12, so a generic validator ignores it while Xeto-aware tooling can recover the real type.
Here is an example of what the example specs look like as a JSON-Schema:
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "hx.test.xeto-4.0.6",
"title": "Haxall test library",
"$defs": {
"sys.Marker": {
"const": "\u2713",
"description": "Marker labels a dict with typing information",
"x-xeto": {
"spec": "sys::Marker"
}
},
"sys.Ref": {
"type": "string",
"pattern": "^(?:[a-zA-Z\\d\\._~]*)$",
"description": "Reference to another dict. The `of` meta defines target type",
"x-xeto": {
"spec": "sys::Ref"
}
},
"sys.Entity": {
"description": "Top-level dicts with a unique identifier.",
"x-xeto": {
"spec": "sys::Entity",
"abstract": true
},
"additionalProperties": true,
"type": "object",
"properties": {
"id": {
"description": "Unique identifier for entity in its project",
"$ref": "#/$defs/sys.Ref"
},
"spec": {
"description": "Type of this entity",
"x-xeto": {
"of": "sys::Spec"
},
"$ref": "#/$defs/sys.Ref"
}
},
"required": [
"id"
]
},
"sys.Float": {
"anyOf": [
{
"type": "number"
},
{
"enum": [
"NaN",
"INF",
"-INF"
]
}
],
"description": "Unitless floating point number",
"x-xeto": {
"spec": "sys::Float",
"unitless": true
}
},
"sys.DateTime": {
"type": "string",
"pattern": "^(?:\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d+)*[Z+-][\\d:]*[ ]*[-+a-zA-Z_\\d]*)$",
"description": "ISO 8601 timestamp followed by timezone identifier",
"x-xeto": {
"spec": "sys::DateTime"
}
},
"hx.test.xeto.OrderType": {
"x-xeto": {
"spec": "hx.test.xeto::OrderType"
},
"type": "string",
"enum": [
"kitchen",
"bathRoom",
"livingRoom",
"secretLab"
]
},
"hx.test.xeto.Product": {
"x-xeto": {
"spec": "hx.test.xeto::Product"
},
"additionalProperties": true,
"type": "object",
"properties": {
"product": {
"$ref": "#/$defs/sys.Marker"
},
"price": {
"$ref": "#/$defs/sys.Float"
},
"name": {
"type": "string"
}
},
"required": [
"product",
"name",
"price"
]
},
"hx.test.xeto.Order": {
"allOf": [
{
"$ref": "#/$defs/sys.Entity"
},
{
"additionalProperties": true,
"type": "object",
"properties": {
"orderType": {
"$ref": "#/$defs/hx.test.xeto.OrderType"
},
"id": {
"description": "Unique identifier for entity in its project",
"$ref": "#/$defs/sys.Ref"
},
"orderDate": {
"$ref": "#/$defs/sys.DateTime"
},
"items": {
"type": "array",
"items": {
"$ref": "#/$defs/hx.test.xeto.Product"
}
},
"spec": {
"description": "Type of this entity",
"x-xeto": {
"of": "sys::Spec"
},
"$ref": "#/$defs/sys.Ref"
},
"customerName": {
"type": "string"
},
"order": {
"$ref": "#/$defs/sys.Marker"
}
},
"required": [
"id",
"order",
"customerName",
"orderDate",
"orderType",
"items"
]
}
],
"x-xeto": {
"spec": "hx.test.xeto::Order"
}
}
}
}