Data format
The complete reference for the SmartCompanion data format. The normative source is the JSON Schema:
- v1.1 — https://smartcompanion-app.github.io/data-format/v1.1/schema.json (current)
- v1.0 — https://smartcompanion-app.github.io/data-format/v1.0/schema.json
The specification and its examples live in smartcompanion-app/data-format.
Versioning
The schema is versioned major.minor.
| Bump | Meaning |
|---|---|
| minor | Additive only. A file valid against v1.0 is still valid against v1.1. |
| major | Breaking. A file valid against the old major may not validate against the new one. |
| Version | What it added |
|---|---|
| v1.0 | The core: checksum, assets, stations, languages, texts, plus optional tours, pins and server. |
| v1.1 | The optional share attribute — a URL for recommending the guide. |
There is no version field inside the file. A v1.0 file is a valid v1.1 file, so the app reads either.
Top-level object
The document is a single JSON object. additionalProperties is false: any key not listed here is a validation error.
| Field | Type | Required | Description |
|---|---|---|---|
checksum | string | yes | Change-detection token |
assets | array | yes | Every media file, with its URL. At least one. |
stations | array | yes | The points of interest. At least one. |
languages | array | yes | Selectable languages. At least one. |
texts | array | yes | Interface strings. At least one. |
tours | array | no | Groupings of stations |
pins | array | no | Four-digit access codes |
server | array | no | Local server IPs for faster in-house downloads |
share | string | no | URL offered in the share sheet (v1.1) |
checksum
"checksum": "2026-10-01-001"
An arbitrary string the app uses to detect that the content changed.
The app keeps the content it loaded in the device's storage. On every start it fetches data.json again, but it only replaces what it stored when the checksum differs from the stored one. That holds online and offline alike.
Change it every time you edit the file. Any value works — a date, a counter, a git hash. What matters is that it differs from the previous one.
Editing content and leaving checksum alone. Every visitor who opened the guide before keeps the old content, because as far as their device can tell nothing changed. If an update did not show up, check this first.
assets
Every downloadable file — audio, images — is declared once here and referenced by id elsewhere.
{
"id": "a1de",
"language": "de",
"title": "Der Bär",
"duration": 92,
"filename": "a1de.mp3",
"externalUrl": "https://example.org/assets/a1de.mp3"
}
| Field | Type | Required | Description |
|---|---|---|---|
id | string | yes | Unique across all assets. Referenced from stations.images, stations.audios and tours.images. |
filename | string | yes | File name with extension, no path. The local name when cached offline. |
externalUrl | string | yes | The URL the file is downloaded from — absolute, or relative to the app for bundled content. |
language | string | no | Two- or three-letter code, e.g. de. Set it on audio; leave it off images. |
title | string | no | Display title, e.g. an audio track name |
duration | number | no | Length in seconds. Allowed, but the app does not read it: the player takes the length from the file. |
Conventions worth following
- Keep
filenameunique across the whole file. It becomes the name on disk in offline mode. - Set
languageon audio, leave it off images. Images are normally shared between the language variants of a station.
stations
A station is a point of interest. One entry per language: the same id and number, a different language, title and audios.
{
"id": "s1",
"language": "de",
"number": "1",
"title": "Bär",
"subtitle": "Ursus arctos",
"description": "Der Braunbär war …",
"latitude": 47.2692,
"longitude": 11.4041,
"images": ["i11", "i12"],
"audios": ["a1de"]
}
| Field | Type | Required | Description |
|---|---|---|---|
id | string | yes | Shared across the language variants of the same station |
language | string | yes | Which language this variant is in. Pattern ^[a-z]{2,3}$. |
number | string | yes | What the visitor types on the keypad and reads on the sign. A string. |
title | string | yes | Station name in this language |
images | array of string | yes | Asset ids of the station's pictures. At least one; the default app needs two. |
subtitle | string | no | Secondary line under the title |
description | string | no | Longer text shown with the station |
audios | array of string | no | Asset ids of the audio for this station in this language. The default app needs exactly one. |
latitude | number | no | −90 to 90. Only needed for a map. |
longitude | number | no | −180 to 180 |
Stations covers the practical side — numbering, ordering, and what the default app expects beyond the schema.
languages
"languages": [
{ "language": "de", "title": "Deutsch" },
{ "language": "en", "title": "English" }
]
| Field | Type | Required | Description |
|---|---|---|---|
language | string | yes | Code, pattern ^[a-z]{2,3}$ |
title | string | yes | The language's name written in that language — Deutsch, not German |
With a single language, the app selects it and skips the language screen. With several, the visitor picks one on first launch — or the link they opened chose it already, see Languages.
texts
Interface strings, as flat (language, key, value) triples.
{ "language": "en", "key": "menu-overview", "value": "Overview" }
| Field | Type | Required | Description |
|---|---|---|---|
language | string | yes | Code, pattern ^[a-z]{2,3}$ |
key | string | yes | Identifier the app looks up |
value | string | yes | Translated text |
Provide every key for every language you list. A missing key does not fail; the app shows a stand-in made from the key — lower-cased, with its first - and its first _ turned into spaces, so menu-overview reads menu overview — which a visitor will notice. UI texts lists every key.
tours
Optional. A tour groups stations into a route. Like stations, one entry per language.
{
"id": "tour1",
"language": "de",
"title": "Rundgang durch den Zoo",
"description": "Ein Rundgang von 45 Minuten.",
"number": "1",
"default": true,
"stations": ["s1", "s2", "s3"],
"images": ["i11"]
}
| Field | Type | Required | Description |
|---|---|---|---|
id | string | yes | Shared across language variants |
language | string | yes | Which language this variant is in |
title | string | yes | Tour name in this language |
default | boolean | yes | Exactly one tour should be true |
stations | array of string | yes | The stations in this tour. At least one. |
images | array of string | yes | Asset ids for the tour |
number | string | no | Tour number, for visitors |
description | string | no | Longer text |
tours.stations holds station idsThe library looks each entry up as a station id. The schema's description calls them station numbers; in data where ids and numbers coincide, as in most examples, the difference never shows. Where they differ, write ids.
The default app does not show tours; they are used by apps that route to the tour pages. See Tours.
pins
Optional. Four-digit codes that gate access to the guide.
"pins": ["1234", "5678"]
Each entry must match ^[0-9]{4}$. If the array is present, the app shows a PIN screen after loading and continues only once the visitor enters one of the listed codes. A correct code is remembered on the device for six hours by default.
This is a turnstile, not a security control — the codes sit in a file anyone can download. See Access and sharing.
server
Optional. IP addresses of servers on the venue's own network.
"server": ["192.168.1.50"]
Meant for local downloads on the venue network. Two caveats: the default app does not use the list, and the library reads it from a key named servers, not server — so a file that follows the schema's spelling never delivers it. See Access and sharing.
share
Added in v1.1. Optional. A URL offered when the visitor uses the share item in the menu.
"share": "https://guide.example.org/"
Normally the public URL of your deployment. The menu item only appears when this is set and the device can share. See Access and sharing.
Checklist
A schema validator checks shape. These are the things it cannot check, and they are where the real bugs live:
- Every id in
stations.imagesandstations.audiosexists inassets - Every id in
tours.imagesexists inassets - Every entry in
tours.stationsis an existing stationid - Every station exists in every language listed in
languages - Every
textskey exists in every language - Every
externalUrlloads over HTTPS, with CORS allowed if it is on another host — see Media -
checksumdiffers from the previously published version - Exactly one tour has
"default": true, if you use tours
A short script covers the reference checks:
import json
import sys
d = json.load(open(sys.argv[1], encoding="utf-8"))
asset_ids = {a["id"] for a in d["assets"]}
languages = {l["language"] for l in d["languages"]}
station_ids = {s["id"] for s in d["stations"]}
problems = []
for s in d["stations"]:
for ref in s.get("images", []) + s.get("audios", []):
if ref not in asset_ids:
problems.append("station %s/%s references unknown asset %s" % (s["id"], s["language"], ref))
for t in d.get("tours", []):
for ref in t["stations"]:
if ref not in station_ids:
problems.append("tour %s references unknown station %s" % (t["id"], ref))
for ref in t["images"]:
if ref not in asset_ids:
problems.append("tour %s references unknown asset %s" % (t["id"], ref))
by_language = {}
for s in d["stations"]:
by_language.setdefault(s["language"], set()).add(s["id"])
for language in languages:
for missing in sorted(station_ids - by_language.get(language, set())):
problems.append("station %s missing in %s" % (missing, language))
keys = {t["key"] for t in d["texts"]}
for language in languages:
present = {t["key"] for t in d["texts"] if t["language"] == language}
for missing in sorted(keys - present):
problems.append("text %s missing in %s" % (missing, language))
print("\n".join(problems) or "no problems found")
python check.py data.json