---
updatedAt: 2026-08-26T14:30:03.000Z
agentTools:
  projectIndex: https://dev.xyte.io/llms.txt
---

# Switching the schema per firmware generation

A single model often has units in the field on very different firmware. A newer generation may add a DSP block and drop a legacy audio section, so one flat schema either shows fields a device doesn't have or hides fields it does.

Instead of publishing a second model per firmware, keep **one** model and let the device tell you which shape of configuration it speaks. The device reports a `schema_version` value via `set-config`, and the schema swaps whole sections in and out based on it.

## How it works

* Each firmware generation gets its own subschema under `definitions` (`v1`, `v2`, …), holding only the properties that generation actually has.
* One `if` / `then` block per generation lives inside `allOf`, and `then` is just a `$ref` to that generation's definition. Adding a third generation is one new definition plus one new `if` / `then` block — nothing existing changes.
* The trigger value is free-form, so `schema_version` can be a version number (`"2.0.0"`), a plain label (`"Old"` / `"New"`), or a firmware/model codename (`"Gen2"`). List every accepted value in that branch's `enum` and they all select the same subschema.
* Properties declared at the top level (outside `allOf`) are shared by every generation. A device reporting an unrecognised `schema_version` therefore still renders the common fields and simply gets no generation-specific sections.
* At the top level, use `allOf` with `if` / `then` rather than a single `if` / `then` / `else` pair once you have more than two generations, and avoid a top-level `oneOf` — the editor renders `oneOf` as an option picker rather than switching sections. Inside a generation's definition, the opposite applies: see the warning below.

> ⚠️ Every `$id` in the schema must be unique
>
> `$id` values are registered document-wide. If two branches (`then` and `else`, or two `definitions`) declare the same `$id` — for example both defining `"$id": "#/properties/about"` — the schema fails to compile and the condition silently evaluates as **false**, so the form always falls through to the other branch with no error shown. Either give every branch a distinct `$id` or omit `$id` inside the branches entirely; it is not required for the form to render.

> ⚠️ Do not nest `allOf` inside a generation's definition
>
> Keep `allOf` at the top level, for the version branches only. An `allOf` placed inside a `definitions` subschema is flattened into that top-level merge, which cannot resolve the `if` keyword and fails with `No resolver found for key if`. The failure is swallowed, and the **entire** top-level `allOf` is discarded — so no generation-specific section renders at all, for any version. For conditional logic inside a definition, use nested `if` / `then` / `else` instead:
>
> ```json
> "security": {
>   "type": "object",
>   "title": "Security",
>   "properties": {
>     "touch_lock": { "type": "boolean", "title": "Touch Lock", "default": false },
>     "lock_ir":    { "type": "boolean", "title": "IR Lock",    "default": false }
>   },
>   "if":   { "properties": { "touch_lock": { "const": true } } },
>   "then": { "properties": { "lock_ir": { "readOnly": true } } },
>   "else": {
>     "if":   { "properties": { "lock_ir": { "const": true } } },
>     "then": { "properties": { "touch_lock": { "readOnly": true } } }
>   }
> }
> ```
>
> An `if` / `then` / `else` block directly on an object property is resolved before any merging happens, so it is safe at any depth. It is only `allOf` under a definition that breaks.

> 📘 Draft support
>
> Conditional keywords (`if` / `then` / `else`, `allOf`, `$ref`) come from JSON Schema draft-07, which the model's Config Schema editor supports. Drafts above 7 are not supported.

## Config Schema

```json Config Schema
{
  "type": "object",
  "title": "Device Configuration",
  "properties": {
    "schema_version": {
      "type": "string",
      "title": "Config Schema Version",
      "readOnly": true
    },
    "about": {
      "type": "object",
      "title": "About",
      "properties": {
        "model": { "type": "string", "title": "Model", "readOnly": true },
        "firmware": { "type": "string", "title": "Firmware", "readOnly": true }
      }
    }
  },
  "definitions": {
    "v1": {
      "properties": {
        "legacy_audio": {
          "type": "object",
          "title": "Audio (legacy firmware)",
          "properties": {
            "gain": { "type": "integer", "title": "Input Gain (dB)", "minimum": -20, "maximum": 20 },
            "mono_mixdown": { "type": "boolean", "title": "Mono Mixdown" }
          }
        }
      }
    },
    "v2": {
      "properties": {
        "dsp": {
          "type": "object",
          "title": "DSP (2.0 firmware)",
          "properties": {
            "eq_preset": { "type": "string", "title": "EQ Preset", "enum": ["flat", "voice", "music"] },
            "auto_gain": { "type": "boolean", "title": "Auto Gain Control" },
            "delay_ms": { "type": "integer", "title": "Output Delay (ms)", "minimum": 0, "maximum": 500 }
          }
        },
        "network": {
          "type": "object",
          "title": "Network (2.0 firmware)",
          "properties": {
            "dhcp": { "type": "boolean", "title": "DHCP" },
            "hostname": { "type": "string", "title": "Hostname" }
          }
        }
      }
    }
  },
  "allOf": [
    {
      "if": {
        "properties": { "schema_version": { "enum": ["1.0.0", "Old", "Gen1"] } },
        "required": ["schema_version"]
      },
      "then": { "$ref": "#/definitions/v1" }
    },
    {
      "if": {
        "properties": { "schema_version": { "enum": ["2.0.0", "New", "Gen2"] } },
        "required": ["schema_version"]
      },
      "then": { "$ref": "#/definitions/v2" }
    }
  ]
}
```

## What the device reports

The device sets its configuration through the [Set Config](https://dev.xyte.io/reference/set-config) endpoint, including `schema_version` alongside the sections that its firmware supports.

```json Legacy firmware
{
  "schema_version": "Old",
  "about": {
    "model": "AVX-4200",
    "firmware": "1.1.16"
  },
  "legacy_audio": {
    "gain": 3,
    "mono_mixdown": false
  }
}
```

```json 2.0 firmware
{
  "schema_version": "2.0.0",
  "about": {
    "model": "AVX-4200",
    "firmware": "2.4.1"
  },
  "dsp": {
    "eq_preset": "voice",
    "auto_gain": true,
    "delay_ms": 20
  },
  "network": {
    "dhcp": true,
    "hostname": "avx4200-lab"
  }
}
```

A unit reporting `"Old"` shows **About** and **Audio (legacy firmware)**; a unit reporting `"2.0.0"` shows **About**, **DSP (2.0 firmware)** and **Network (2.0 firmware)** — from the same model and the same schema. A unit reporting `"Gen2"` resolves to the same sections as `"2.0.0"`, since both values appear in that branch's `enum`.