A schema declares which members of a native type the page can access and how each field is exposed to markup.

## Mapping Types to Schemas

Choose how to describe a type based on how it is defined in native code:

| Native Type | What to Use |
|---|---|
| Existing class | `dd::TypeTraits` specialization |
| Type written for the UI | `schema` member |
| Plain struct | Automatic binding (no schema) |
| Plain struct with actions or editable fields | `dd::Reflect` in a `dd::TypeTraits` specialization |

### A TypeTraits Specialization

You can define schemas for existing classes by specializing `dd::TypeTraits`:

```cpp
// An existing class that we want to expose to markup:
class Player {
 public:
  const std::string& name() const;
  int level() const;
  const Stats& stats() const;       // simple structs marshal via reflection
};

// Define a schema for "Player" by specializing dd::TypeTraits<Player>
template <> struct dd::TypeTraits<Player> {
  static constexpr auto schema = dd::Schema(
      dd::Field("name", &Player::name),     // player.name
      dd::Field("level", &Player::level),   // player.level
      dd::Field("stats", &Player::stats));  // player.stats
};
```

### A Schema Member

You can also define schemas inside structs or classes by defining a `schema` member:

```cpp
struct Settings {
  double volume = 0.8;
  bool subtitles = true;

  static constexpr auto schema = dd::Schema(
      dd::Field("volume", &Settings::volume),
      dd::Field("subtitles", &Settings::subtitles));
};
```

This keeps field declarations beside the data members they expose.

### Plain Structs

Simple structs bind automatically via reflection, with each field exposed under its C++ member name:

```cpp
// No schema needed (marshalled automatically):
struct Stats {
  int health = 100;  // stats.health
  int armor = 0;     // stats.armor
};
```

> 📘 Compiler Requirements
>
> Automatic reflection requires Clang, GCC, or MSVC 19.40+. The struct must be a plain aggregate with at most 24 members, and without custom constructors, virtual functions, or C array members.

### Reflect, Then Annotate

For the sake of convenience (and to add extra annotations where needed), you can combine reflection with explicit schemas by using `dd::Reflect` inside a `dd::TypeTraits` specialization:

```cpp
struct Toolbar {
  std::string url;
  double progress = 0;
};

template <> struct dd::TypeTraits<Toolbar> {
  static constexpr auto schema = dd::Schema(
      dd::Reflect<Toolbar>(),  // url and progress
      dd::Action("reload"));
};
```

Because `dd::Reflect` requires a complete type, define it in a `dd::TypeTraits` specialization rather than inside the struct itself.

## Declaring Custom Fields

Pass `dd::Field` the property name (for exposing to markup) and a custom accessor to obtain the value:

```cpp
struct Enemy {
  std::string name;
  int hp = 0;
  int max_hp = 100;
  bool IsBoss() const;
};

template <> struct dd::TypeTraits<Enemy> {
  static constexpr auto schema = dd::Schema(
      dd::Field("name", &Enemy::name),    // a data member
      dd::Field("boss", &Enemy::IsBoss),  // a const member function
      dd::Field("hpPct", [](const Enemy& e) {
        return 100.0 * e.hp / e.max_hp;   // a custom, computed value
      }));
};
```

An accessor can be a member pointer, a const member function, or a stateless lambda. Lambda accessors compute values on the fly— define them in a `dd::TypeTraits` specialization because the class must be complete.

The return type of the accessor determines how the property behaves on the page:

| Field Type | What the Page Receives |
|---|---|
| Value type (`bool`, number, `std::string`, enum, `Color`) | Bound value shown on the page |
| Described type | Nested object reached with dot notation |
| `std::optional` or pointer to a described type | Nested object that can be absent (null) |
| Container of described rows | List bound to repeating elements |

### Nested Objects

Describing a schema with an existing, described type (or simple struct) exposes the type as a nested object in page markup:

```cpp
struct Profile {
  std::string name;  // profile.name
  Stats stats;       // { profile.stats.armor, profile.stats.health }

  static constexpr auto schema = dd::Schema(
      dd::Field("name", &Profile::name),
      dd::Field("stats", &Profile::stats));
};
```

In markup, access nested fields with dot notation:

```html
<p>Armor: {{profile.stats.armor}}</p>
```

### Optional Objects

An `std::optional` or pointer to a described type is null on the page while empty:

```cpp
struct MatchResult {
  int kills = 0;
};

struct Profile {
  std::optional<MatchResult> lastMatch;  // absent until the first match

  static constexpr auto schema = dd::Schema(
      dd::Field("lastMatch", &Profile::lastMatch));
};
```

Use `ul-if` to show an element only when the object is present:

```html
<p ul-if="profile.lastMatch">Kills: {{profile.lastMatch.kills}}</p>
```

### Enums

Enums bind automatically as strings using their enumerator names:

```cpp
enum class Rank { Bronze, Silver, Gold };

struct Badge {
  Rank rank = Rank::Gold;  // the page sees "Gold"
};
```

The page receives the name as text, which works directly in attributes and CSS selectors:

```html
<div class="badge" ul-attr:data-rank="badge.rank"></div>
```

The library reflects values from 0 to 63 by default. Specialize `dd::EnumRange` if an enum uses values outside that range:

```cpp
template <> struct dd::EnumRange<ErrorCode> {
  static constexpr long long min = 0, max = 255;
};
```

### Custom Value Types

To expose custom engine types as primitive values on the page, specialize `dd::ValueTraits`:

```cpp
template <> struct dd::ValueTraits<Fixed> {
  static constexpr dd::ValueKind kind = dd::ValueKind::Double;
  static double ToSlot(const Fixed& value) { return value.ToDouble(); }
};
```

`ToSlot()` converts the custom type into a supported primitive before the value reaches the page.

A change handler registered for a `dd::ValueTraits` field accepts either the underlying primitive or a `dd::Value`— it never receives the custom type.

When an action uses a `dd::ValueTraits` type as its payload, handlers receive the value only as a `dd::Value`.

## Declaring Lists

Declare a list using `dd::List`, passing the collection accessor and a member pointer that identifies each row:

```cpp
struct Item {
  int64_t id = 0;
  std::string name;
  int count = 0;
};

struct Inventory {
  std::vector<Item> items;

  static constexpr auto schema = dd::Schema(
      dd::List("items", &Inventory::items, &Item::id));  // keyed by id
};
```

In markup, place `ul-for` on the container element and put row markup inside a `<template>` tag (a standard HTML tag whose contents the browser parses but never shows; the page copies it once per row):

```html
<ul ul-for="inventory.items">
  <template>
    <li>{{name}} x{{count}}</li>
  </template>
</ul>
```

Paths inside the template resolve against the row's fields directly.

### Keyed and Positional Lists

Omitting the key identifier matches rows by position rather than identity:

```cpp
struct Line {
  std::string text;
};

struct Chat {
  std::vector<Line> log;

  static constexpr auto schema = dd::Schema(
      dd::List("log", &Chat::log));  // no key: rows match by position
};
```

A keyed list preserves existing DOM elements, input focus, and CSS transitions when items reorder. A positional list suits collections that only append, such as chat or event logs.

For more on lists, see [Binding Lists](/docs/2.0/binding-lists).

## Declaring Actions

Actions let the page request operations in native code without calling functions directly:

```cpp
struct Door {
  bool open = false;

  static constexpr auto schema = dd::Schema(
      dd::Field("open", &Door::open),
      dd::Action("toggle"));  // the page fires door.toggle
};
```

Trigger the action in markup using the `ul-on:click` attribute:

```html
<button ul-on:click="door.toggle">Open or Close</button>
```

Register a native handler to process the queued request during the next sync:

```cpp
binding.OnAction<"toggle">([&] { door.open = !door.open; });
```

### Actions with a Payload

To pass data with an action, specify the payload type as a template parameter:

```cpp
struct Move {
  int from = 0;
  int to = 0;
};

struct Bag {
  static constexpr auto schema = dd::Schema(
      dd::Action<Move>("moveItem"),  // a struct payload
      dd::Action<double>("zoom"));   // a single value
};
```

Actions triggered from markup carry no payload. Payload actions are dispatched programmatically from native code.

For details on subscribing to and emitting actions, see [Handling Actions](/docs/2.0/handling-actions).

## Editable Fields

Marking a field with `dd::Editable` allows form controls on the page to request changes through `ul-value`:

```cpp
struct Settings {
  double volume = 0.8;

  static constexpr auto schema = dd::Schema(
      dd::Field("volume", &Settings::volume, dd::Editable));
};
```

Bind an input element in HTML to the editable field:

```html
<input type="range" min="0" max="1" step="0.01" ul-value="settings.volume">
```

### Applying Changes

Register an `OnChange()` handler to store the requested value:

```cpp
binding.OnChange<"volume">([&](double v) { settings.volume = v; });
```

The library never modifies native data directly— change requests are queued until `Sync()` runs the handler. Editable fields apply only to the bound root type, not to nested objects or list rows.

To learn how change requests work with form controls, see [Binding Form Controls](/docs/2.0/binding-form-controls).

## CSS Variables

Use `dd::Var` to publish numeric or color values directly to CSS custom properties without extra markup:

```cpp
struct Vitals {
  double stamina = 1.0;  // 0 to 1

  static constexpr auto schema = dd::Schema(
      dd::Var("--stamina", &Vitals::stamina));
};
```

Reference the property in CSS stylesheets using the standard `var()` function:

```css
.stamina-bar { width: calc(var(--stamina) * 100%); }
```

On a bound root type, the property is set on the document's root element. On a row type, it is set on each row's root element. For more on styling with bindings, see [Showing Data in Markup](/docs/2.0/showing-data-in-markup).

> 👍 Compile-Time Validation
>
> Schema mistakes fail at compile time rather than runtime. Invalid names, duplicate entries, or annotations for missing fields produce compiler errors that name the broken rule, such as `SchemaErrorDuplicateEntryName`.
>
> Entry names for fields, lists, and actions must start with a letter or an underscore, followed by letters, digits, and underscores (eg, `maxHealth` or `slot_2`). A `dd::Var` name must start with two hyphens followed by lowercase letters, digits, and hyphens (eg, `--max-hp`).
