Authoring guide
Producing a conformant .odio.json
This guide shows three ways to author an ODIO device file — by hand, with the TypeScript SDK, or via the Genie importer — and how to validate the result. A file is conformant if and only if it validates against the canonical schema for its kind.
What you are producing
One .odio.json file per orderable model. It is a single JSON object with a required odioVersion, id, device identity, and ports array, plus optional power, physical, standards, parameters, and provenance blocks. Start every file by pointing $schema at the canonical URL so editors and validators resolve it:
skeleton{ "$schema": "https://opendeviceio.org/schema/v0.1/device.schema.json", "odioVersion": "0.1.0", "id": "acme/widget-100", "device": { "manufacturer": "Acme", "model": "Widget 100" }, "ports": [] }
The connector / link / signals model
Each port separates three things. Get this right and the rest follows:
- connector — the physical jack only, from the controlled vocabulary (
rj45,hdmi-type-a,usb-c,phoenix,xlr-3-f…). Unknown connectors use"connector": "other"plus free text so a missing term never blocks a file. A multi-pole terminal block records its physicalpoleCount. - link — the physical pipe:
type,standard,speed/bandwidthGbps, and link-level facts such as PoE ({ standard, role, classWatts }) or USBpowerDeliveryWatts. State it once per port. - signals — the concurrent logical flows. One connector may carry several: an HDMI port carries
video+audio+control(CEC); one RJ45 can carrydante+aes67+ LAN. Keep the physical pole count (on the connector) separate from the number of logical circuits (signal.channels).
The canonical worked example of that last point: a 3-pole Phoenix RS-232 port is one connector with one control circuit, whereas an 8-pole Phoenix GPIO header is one connector carrying eight independent control circuits (channels: 8).
A worked example
An HDMI input that carries video and embedded audio, plus a phoenix GPIO header showing the pole-count-vs-channels distinction:
ports"ports": [ { "id": "hdmi-in-1", "label": "HDMI INPUT 1", "direction": "input", "connector": "hdmi-type-a", "count": 1, "link": { "type": "hdmi", "standard": "hdmi-2.0", "bandwidthGbps": 18 }, "location": { "face": "rear", "group": "inputs", "order": 1 }, "signals": [ { "domain": "video", "transport": "hdmi", "maxResolution": "4096x2160", "maxRefreshHz": 60, "hdcp": "2.2" }, { "domain": "audio", "transport": "lpcm", "maxChannelsPerCircuit": 8 } ] }, { "id": "gpio", "label": "GPIO", "direction": "bidirectional", "connector": "phoenix", "poleCount": 8, "signals": [ { "domain": "control", "transport": "gpio", "channels": 8 } ] }, { "id": "lan", "label": "LAN", "direction": "bidirectional", "connector": "rj45", "link": { "type": "ethernet", "standard": "1000base-t", "speed": "1g", "poe": { "standard": "802.3at", "role": "pd", "classWatts": 30 } }, "signals": [ { "domain": "network", "transport": "control-network", "managed": true }, { "domain": "control", "transport": "ip-control" } ] } ]
The id slug rule
The id is the stable join key. It is derived as slug(manufacturer)/slug(model)[@slug(revision)]. The slug rule:
- lowercase everything;
- replace
+with-plus; - collapse runs of any other character outside
[a-z0-9._-]into a single-.
So Lightware / UCX-4x2-HC60D becomes lightware/ucx-4x2-hc60d, and Extron / DTP2 T 211 revision A becomes extron/dtp2-t-211@a.
The ^x- extension rule
The core schema sets additionalProperties: false, so an unrecognized non-extension key makes the file invalid — drift is caught, not silently accepted. To add vendor- or tool-specific data, use a key matching ^x- at any object level; validators MUST ignore unknown x- keys.
extension keys{ "id": "acme/widget-100", "device": { "manufacturer": "Acme", "model": "Widget 100" }, "ports": [ /* ... */ ], "x-dtools": { "category": "Switchers" }, "x-note": "Free-form note for reviewers; ignored by validators." }
Authoring with the SDK
@opendeviceio/sdk ships the generated TypeScript types, an Ajv 2020 validator, and convenience accessors. Author the object with full type-checking, then validate:
typescriptimport { type OdioDevice, validateDocument, formatErrors, inputPorts, poeBudget } from "@opendeviceio/sdk"; const device: OdioDevice = { $schema: "https://opendeviceio.org/schema/v0.1/device.schema.json", odioVersion: "0.1.0", id: "acme/widget-100", device: { manufacturer: "Acme", model: "Widget 100" }, ports: [ { id: "lan", label: "LAN", direction: "bidirectional", connector: "rj45", link: { type: "ethernet", standard: "1000base-t", speed: "1g" }, signals: [{ domain: "network", transport: "control-network" }] } ] }; // validateDocument routes on `kind` (device | bundle | cable). const result = validateDocument(device); if (!result.valid) { console.error(formatErrors(result.errors)); process.exit(1); } // Accessors derive useful facts straight from the document: console.log(inputPorts(device).length, "input-capable ports"); console.log(poeBudget(device), "W of PoE source budget");
Other accessors include outputPorts, signalsByDomain, signalsByTransport, estimatedBtuPerHour, rackUnits, and — for kits — flattenBundle and bundleBillOfMaterials.
Authoring with Genie
Genie is the reference importer: it reads a product PDF and emits a draft .odio.json plus a review report flagging low-confidence fields. The intended workflow is generate → human-review → publish, promoting the document's provenance.validation.status from draft to reviewed (or manufacturer-verified) as it is checked.
shell# Generate a draft from a datasheet, with a review report: genie parse datasheet.pdf -o widget-100.odio.json --review-report review.md # The Claude API key is supplied at runtime via an env var; Genie never ships one.
Validating your file
Validation is the whole contract. Use the SDK's validateDocument programmatically (above), or the repo's conformance runner to validate a directory of examples against the schema with Ajv 2020:
shell# From the repo root — validates every examples/*.odio.json and confirms the # examples/invalid/* fixtures fail, exactly as CI does: npm install npm run validate:examples # node tools/validate-examples.mjs
Once your file validates, you can browse the registry to see how published devices, bundles, and cables render, or fetch the canonical device, bundle, and cable schemas directly.