Skip to content

Commit 41834c1

Browse files
authored
proposal: Introduce validation framework and data protocol enhancements (#438)
1 parent d799665 commit 41834c1

24 files changed

+1683
-520
lines changed

specification/0.9/docs/a2ui_protocol.md

Lines changed: 78 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -82,8 +82,8 @@ A2UI v0.9 is defined by three interacting JSON schemas.
8282

8383
The [`common_types.json`] schema defines reusable primitives used throughout the protocol.
8484

85-
- **`stringOrPath` / `numberOrPath` / `booleanOrPath` / `stringArrayOrPath`**: The core of the data binding system. Any property that can be bound to data is defined as an object that accepts either a literal value OR a `path` string ([JSON Pointer]).
86-
- **`childrenProperty`**: Defines how containers hold children. It supports:
85+
- **`DynamicString` / `DynamicNumber` / `DynamicBoolean` / `DynamicStringList`**: The core of the data binding system. Any property that can be bound to data is defined as a `Dynamic*` type. It accepts either a literal value, a `path` string ([JSON Pointer]), or a `FunctionCall` (function call).
86+
- **`ChildList`**: Defines how containers hold children. It supports:
8787

8888
- `array`: A static array of string component IDs.
8989
- `object`: A template for generating children from a data binding list (requires a template `componentId` and a data binding `path`).
@@ -169,8 +169,7 @@ This message is used to send or update the data that populates the UI components
169169

170170
- `surfaceId` (string, required): The unique identifier for the UI surface this data model update applies to.
171171
- `path` (string, optional): A JSON Pointer to a specific location within the data model (e.g., `/user/name`). If omitted or set to `/`, the entire data model for the surface will be replaced.
172-
- `op` (string, optional): The operation to perform on the data model. Must be 'add', 'replace' or 'remove'. If omitted, defaults to 'replace'.
173-
- `value` (object): The data to be updated in the data model. This can be any valid JSON object. Required if `op` is 'add' or 'replace'. Not allowed if `op` is 'remove'.
172+
- `value` (object): The data to be updated in the data model. If present, the value at `path` is updated/created. If this field is omitted, the data at `path` is **removed**.
174173

175174
**Example:**
176175

@@ -179,7 +178,6 @@ This message is used to send or update the data that populates the UI components
179178
"updateDataModel": {
180179
"surfaceId": "user_profile_card",
181180
"path": "/user",
182-
"op": "replace",
183181
"value": {
184182
"name": "Jane Doe",
185183
"title": "Software Engineer"
@@ -212,8 +210,8 @@ The following example demonstrates a complete interaction to render a Contact Fo
212210

213211
```jsonl
214212
{"createSurface":{"surfaceId":"contact_form_1","catalogId":"https://a2ui.dev/specification/0.9/standard_catalog_definition.json"}}
215-
{"updateComponents":{"surfaceId":"contact_form_1","components":[{"id":"root","component":"Column","children":["first_name_label","first_name_field","last_name_label","last_name_field","email_label","email_field","phone_label","phone_field","notes_label","notes_field","submit_button"]},{"id":"first_name_label","component":"Text","text":"First Name"},{"id":"first_name_field","component":"TextField","label":"First Name","text":{"path":"/contact/firstName"},"usageHint":"shortText"},{"id":"last_name_label","component":"Text","text":"Last Name"},{"id":"last_name_field","component":"TextField","label":"Last Name","text":{"path":"/contact/lastName"},"usageHint":"shortText"},{"id":"email_label","component":"Text","text":"Email"},{"id":"email_field","component":"TextField","label":"Email","text":{"path":"/contact/email"},"usageHint":"shortText"},{"id":"phone_label","component":"Text","text":"Phone"},{"id":"phone_field","component":"TextField","label":"Phone","text":{"path":"/contact/phone"},"usageHint":"shortText"},{"id":"notes_label","component":"Text","text":"Notes"},{"id":"notes_field","component":"TextField","label":"Notes","text":{"path":"/contact/notes"},"usageHint":"longText"},{"id":"submit_button_label","component":"Text","text":"Submit"},{"id":"submit_button","component":"Button","child":"submit_button_label","action":{"name":"submitContactForm"}}]}}
216-
{"updateDataModel": {"surfaceId": "contact_form_1", "path": "/contact", "op": "replace", "value": {"firstName": "John", "lastName": "Doe", "email": "john.doe@example.com"}}}
213+
{"updateComponents":{"surfaceId":"contact_form_1","components":[{"id":"root","component":"Column","children":["first_name_label","first_name_field","last_name_label","last_name_field","email_label","email_field","phone_label","phone_field","notes_label","notes_field","submit_button"]},{"id":"first_name_label","component":"Text","text":"First Name"},{"id":"first_name_field","component":"TextField","label":"First Name","value":{"path":"/contact/firstName"},"variant":"shortText"},{"id":"last_name_label","component":"Text","text":"Last Name"},{"id":"last_name_field","component":"TextField","label":"Last Name","value":{"path":"/contact/lastName"},"variant":"shortText"},{"id":"email_label","component":"Text","text":"Email"},{"id":"email_field","component":"TextField","label":"Email","value":{"path":"/contact/email"},"variant":"shortText","checks":[{"call":"email","message":"Please enter a valid email address."}]},{"id":"phone_label","component":"Text","text":"Phone"},{"id":"phone_field","component":"TextField","label":"Phone","value":{"path":"/contact/phone"},"variant":"shortText"},{"id":"notes_label","component":"Text","text":"Notes"},{"id":"notes_field","component":"TextField","label":"Notes","value":{"path":"/contact/notes"},"variant":"longText"},{"id":"submit_button_label","component":"Text","text":"Submit"},{"id":"submit_button","component":"Button","child":"submit_button_label","action":{"name":"submitContactForm"}}]}}
214+
{"updateDataModel": {"surfaceId": "contact_form_1", "path": "/contact", "value": {"firstName": "John", "lastName": "Doe", "email": "john.doe@example.com"}}}
217215
```
218216

219217
## Component Model
@@ -283,9 +281,11 @@ By default, all components operate in the **Root Scope**.
283281
- The Root Scope corresponds to the top-level object of the `value` provided in `updateDataModel`.
284282
- Paths starting with `/` (e.g., `/user/profile/name`) are **Absolute Paths**. They always resolve from the root of the Data Model, regardless of where the component is nested in the UI tree.
285283

284+
285+
286286
#### Collection Scopes (Relative Paths)
287287

288-
When a container component (such as `Column`, `Row`, or `List`) utilizes the **Template** feature of `childrenProperty`, it creates a new **Child Scope** for each item in the bound array.
288+
When a container component (such as `Column`, `Row`, or `List`) utilizes the **Template** feature of `ChildList`, it creates a new **Child Scope** for each item in the bound array.
289289

290290
- **Template Definition:** When a container binds its children to a path (e.g., `path: "/users"`), the client iterates over the array found at that location.
291291
- **Scope Instantiation:** For every item in the array, the client instantiates the template component.
@@ -341,7 +341,7 @@ When a container component (such as `Column`, `Row`, or `List`) utilizes the **T
341341

342342
### Two-Way Binding & Input Components
343343

344-
Interactive components that accept user input (`TextField`, `CheckBox`, `Slider`, `MultipleChoice`, `DateTimeInput`) establish a **Two-Way Binding** with the Data Model.
344+
Interactive components that accept user input (`TextField`, `CheckBox`, `Slider`, `ChoicePicker`, `DateTimeInput`) establish a **Two-Way Binding** with the Data Model.
345345

346346
#### The Read/Write Contract
347347

@@ -362,7 +362,7 @@ It is critical to note that Two-Way Binding is **local to the client**.
362362

363363
- User inputs (keystrokes, toggles) do **not** automatically trigger network requests to the server.
364364
- The updated state is sent to the server only when a specific **User Action** is triggered (e.g., a `Button` click).
365-
- When a `userAction` is dispatched, the `context` property of the action can reference the modified data paths to send the user's input back to the server.
365+
- When a `action` is dispatched, the `context` property of the action can reference the modified data paths to send the user's input back to the server.
366366

367367
#### Example: Form Submission Pattern
368368

@@ -379,7 +379,61 @@ It is critical to note that Two-Way Binding is **local to the client**.
379379
}
380380
```
381381

382-
4. **Send:** When clicked, the client resolves `/formData/email` (getting "jane@example.com") and sends it in the `userAction` payload.
382+
4. **Send:** When clicked, the client resolves `/formData/email` (getting "jane@example.com") and sends it in the `action` payload.
383+
384+
385+
## Client-Side Logic & Validation
386+
387+
A2UI v0.9 generalizes client-side logic into **Functions**. These can be used for validation, data transformation, and dynamic property binding.
388+
389+
### Registered Functions
390+
391+
The client registers a set of named **Functions** (e.g., `required`, `regex`, `email`, `add`, `concat`) in a `FunctionCatalog`. The server references these functions by name. This avoids sending executable code.
392+
393+
Input components (like `TextField`, `CheckBox`) can define a list of checks. Each failure produces a specific error message that can be displayed when the component is rendered. Note that for validation checks, the function must return a boolean.
394+
395+
```json
396+
"checks": [
397+
{
398+
"call": "required",
399+
"args": { "value": { "path": "/formData/zip" } },
400+
"message": "Zip code is required"
401+
},
402+
{
403+
"call": "regex",
404+
"args": {
405+
"value": { "path": "/formData/zip" },
406+
"pattern": "^[0-9]{5}$"
407+
},
408+
"message": "Must be a 5-digit zip code"
409+
}
410+
]
411+
```
412+
413+
### Example: Button Validation
414+
415+
Buttons can also define `checks`. If any check fails, the button is automatically disabled. This allows the button's state to depend on the validity of data in the model.
416+
417+
```json
418+
{
419+
"component": "Button",
420+
"text": "Submit",
421+
"checks": [
422+
{
423+
"and": [
424+
{ "call": "required", "args": { "value": { "path": "/formData/terms" } } },
425+
{
426+
"or": [
427+
{ "call": "required", "args": { "value": { "path": "/formData/email" } } },
428+
{ "call": "required", "args": { "value": { "path": "/formData/phone" } } }
429+
]
430+
}
431+
],
432+
"message": "You must accept terms AND provide either email or phone"
433+
}
434+
]
435+
}
436+
```
383437

384438
## Standard Component Catalog
385439

@@ -448,7 +502,7 @@ If validation fails, the client (or the system acting on behalf of the client) s
448502

449503
The protocol also defines messages that the client can send to the server, which are defined in the [`client_to_server.json`] schema. These are used for handling user interactions and reporting client-side information.
450504

451-
### `userAction`
505+
### `action`
452506

453507
This message is sent when the user interacts with a component that has an `action` defined, such as a `Button`.
454508

@@ -460,11 +514,22 @@ This message is sent when the user interacts with a component that has an `actio
460514
- `timestamp` (string, required): An ISO 8601 timestamp.
461515
- `context` (object, required): A JSON object containing any context provided in the component's `action` property.
462516

517+
### `capabilities`
518+
519+
This message is sent by the client upon connection to inform the server of its capabilities, including supported component catalogs and validation catalogs.
520+
521+
**Properties:**
522+
523+
- `supportedCatalogIds` (array of strings, required): URIs of supported component catalogs.
524+
- `supportedFunctionCatalogIds`: A list of URIs for the function catalogs supported by the client.
525+
- `inlineCatalogs`: An array of inline component catalog definitions provided directly by the client (useful for custom or ad-hoc components).
526+
- `inlineFunctionCatalogs`: An array of function catalog definitions provided directly by the client (useful for custom or ad-hoc functions).
527+
463528
### `error`
464529

465530
This message is used to report a client-side error to the server.
466531

467-
[`standard_catalog_definition.json`]: ../json/standard_catalog_definition.json
532+
[`standard_function_catalog.json`]: ../json/standard_function_catalog.json
468533
[`common_types.json`]: ../json/common_types.json
469534
[`server_to_client.json`]: ../json/server_to_client.json
470535
[`client_to_server.json`]: ../json/client_to_server.json

specification/0.9/docs/evolution_guide.md

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ This document serves as a comprehensive guide to the changes between A2UI versio
1111

1212
### Summary Table
1313

14-
| Feature | v0.8.1 | v0.9 |
14+
| Feature | v0.8.1 | v0.9 |
1515
| :-------------------- | :------------------------------------- | :--------------------------------------------------- |
1616
| **Philosophy** | Structured Output / Function Calling | Prompt-First / In-Context Schema |
1717
| **Message Types** | `beginRendering`, `surfaceUpdate`, ... | `createSurface`, `updateComponents`, ... |
@@ -82,19 +82,21 @@ This document serves as a comprehensive guide to the changes between A2UI versio
8282
**Example:**
8383

8484
**v0.8.1 (`beginRendering`)**:
85+
8586
```json
8687
{
8788
"beginRendering": {
8889
"surfaceId": "user_profile_card",
8990
"root": "root",
9091
"styles": {
91-
"primaryColor": "#007bff",
92+
"primaryColor": "#007bff"
9293
}
9394
}
9495
}
9596
```
9697

9798
**v0.9 (`createSurface`)**:
99+
98100
```json
99101
{
100102
"createSurface": {
@@ -176,7 +178,7 @@ Specifying an unknown surfaceId will cause an error. It is recommended that clie
176178

177179
- **Renamed**: `dataModelUpdate` -> `updateDataModel`.
178180
- **Standard JSON**: The `value` property is now a standard **JSON object**.
179-
- **Op**: The `op` property is added to allow for more complex updates (e.g., `replace`, `remove`).
181+
- **Simplified**: The system relies on upsert semantics, so the client will create or update the data model at the specified path, or remove it if the value is null.
180182
- **Structure**: `{ "name": "Alice" }`
181183
- **Reason**: LLMs are trained to generate JSON objects. Forcing them to generate an "adjacency list" representation of a map was inefficient and error-prone.
182184

@@ -204,17 +206,17 @@ Specifying an unknown surfaceId will cause an error. It is recommended that clie
204206

205207
**v0.9:**
206208

207-
- **Implicit Typing**: `stringOrPath`, `numberOrPath`, etc. are defined in `common_types.json`.
209+
- **Implicit Typing**: `DynamicString`, `DynamicNumber`, etc. are defined in `common_types.json`.
208210
- **Structure**: The schema allows `string` OR `{ "path": "..." }`.
209-
- **Reason**: Much more natural JSON. `{ "text": "Hello" }` is valid. `{ "text": { "path": "/msg" } }` is valid. No need for `{ "text": { "literalString": "Hello" } }`.
211+
- **Reason**: Much more natural JSON. `{ "text": "Hello" }` is valid. `{ "value": { "path": "/msg" } }` is valid. No need for `{ "text": { "literalString": "Hello" } }`.
210212

211213
## 6. Component-Specific Changes
212214

213215
### 6.1. Button Context
214216

215217
**v0.8.1:**
216218

217-
- **Array of Pairs**: `context: [{ "key": "id", "value": "123" }]`
219+
- **Array of Pairs**: `context: [{ "key": "id", "value": { "literalString": "123" } }]`
218220
- **Reason**: Easy to parse, hard to generate.
219221

220222
**v0.9:**
@@ -227,24 +229,26 @@ Specifying an unknown surfaceId will cause an error. It is recommended that clie
227229
**v0.8.1:**
228230

229231
- Property: `textFieldType` (e.g., "email", "password").
232+
- Validation: `validationRegexp`.
230233

231234
**v0.9:**
232235

233-
- Property: **`usageHint`**.
234-
- **Reason**: Consistency with `Text` and `Image` components which already used `usageHint`.
236+
- Property: **`variant`**.
237+
- Validation: **`checks`** (generic list of function calls).
238+
- **Reason**: Consistency with `Text` and `Image` components which already used `variant`. Validation is now more flexible and reusable. Also, `text` was renamed to **`value`** to match other input components.
235239

236240
### 6.3. ChoicePicker (vs MultipleChoice)
237241

238242
**v0.8.1:**
239243

240244
- Component: **`MultipleChoice`**.
241-
- Properties: `selections` (array), `maxAllowedSelections` (integer).
245+
- Properties: `selections` (typed wrapper), `maxAllowedSelections` (integer).
242246

243247
**v0.9:**
244248

245249
- Component: **`ChoicePicker`**.
246-
- Properties: **`value`** (array), **`usageHint`** (enum: `multipleSelection`, `mutuallyExclusive`). The `maxAllowedSelections` property was removed.
247-
- **Reason**: `ChoicePicker` is a more generic name that covers both radio buttons (mutually exclusive) and checkboxes (multiple selection). The `usageHint` controls the behavior, simplifying the component surface area.
250+
- Properties: **`value`** (array), **`variant`** (enum: `multipleSelection`, `mutuallyExclusive`). The `maxAllowedSelections` property was removed.
251+
- **Reason**: `ChoicePicker` is a more generic name that covers both radio buttons (mutually exclusive) and checkboxes (multiple selection). The `variant` controls the behavior, simplifying the component surface area.
248252

249253
### 6.4. Slider
250254

@@ -274,3 +278,20 @@ Specifying an unknown surfaceId will cause an error. It is recommended that clie
274278
}
275279
```
276280
- **Result**: The LLM sees this and can "self-correct" in the next turn.
281+
282+
## 8. Property Rename Summary (Migration Quick Reference)
283+
284+
For developers migrating from earlier versions, here is a quick reference of property renaming:
285+
286+
| Component | Old Name | New Name |
287+
| :----------------- | :--------------------- | :------------- |
288+
| **Row / Column** | `distribution` | `justify` |
289+
| **Row / Column** | `alignment` | `align` |
290+
| **Modal** | `entryPointChild` | `trigger` |
291+
| **Modal** | `contentChild` | `content` |
292+
| **Tabs** | `tabItems` | `tabs` |
293+
| **TextField** | `text` | `value` |
294+
| **Many** | `usageHint` | `variant` |
295+
| **Client Message** | `userAction` | `action` |
296+
| **Client Message** | `clientUiCapabilities` | `capabilities` |
297+
| **Common Type** | `childrenProperty` | `ChildList` |

specification/0.9/eval/package.json

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -19,29 +19,29 @@
1919
"license": "ISC",
2020
"description": "",
2121
"devDependencies": {
22-
"@types/node": "^20.19.25",
22+
"@types/node": "^20.19.27",
2323
"@types/yargs": "^17.0.35",
2424
"dotenv-cli": "^10.0.0",
25-
"prettier": "^3.6.2",
26-
"tsx": "^4.20.6",
25+
"prettier": "^3.7.4",
26+
"tsx": "^4.21.0",
2727
"typescript": "^5.9.3",
2828
"yargs": "^18.0.0"
2929
},
3030
"dependencies": {
31-
"@genkit-ai/ai": "^1.24.0",
32-
"@genkit-ai/compat-oai": "^1.24.0",
33-
"@genkit-ai/core": "^1.24.0",
31+
"@genkit-ai/ai": "^1.27.0",
32+
"@genkit-ai/compat-oai": "^1.27.0",
33+
"@genkit-ai/core": "^1.27.0",
3434
"@genkit-ai/dotprompt": "^0.9.12",
35-
"@genkit-ai/firebase": "^1.24.0",
36-
"@genkit-ai/google-cloud": "^1.24.0",
37-
"@genkit-ai/google-genai": "^1.24.0",
35+
"@genkit-ai/firebase": "^1.27.0",
36+
"@genkit-ai/google-cloud": "^1.27.0",
37+
"@genkit-ai/google-genai": "^1.27.0",
3838
"@types/js-yaml": "^4.0.9",
3939
"ajv": "^8.17.1",
4040
"ajv-formats": "^3.0.1",
41-
"genkit": "^1.24.0",
41+
"genkit": "^1.27.0",
4242
"genkitx-anthropic": "^0.25.0",
4343
"js-yaml": "^4.1.1",
44-
"winston": "^3.18.3",
44+
"winston": "^3.19.0",
4545
"zod": "^3.25.76"
4646
}
4747
}

0 commit comments

Comments
 (0)