ucp-schema 0.1.0

Runtime resolution of UCP schema annotations
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
# ucp-schema

CLI and library for working with UCP-annotated JSON Schemas.

UCP schemas use `ucp_request` and `ucp_response` annotations to define field visibility per operation. This tool resolves those annotations into standard JSON Schema, letting you validate payloads for specific operations (create, read, update, etc.).

## Installation

```bash
# Install from crates.io
cargo install ucp-schema

# Or build from source
git clone https://github.com/Shopify/ucp-schema
cd ucp-schema
cargo install --path .
```

## Quick Start

Given a UCP schema where `id` is omitted on create but required on update:

```json
{
  "type": "object",
  "properties": {
    "id": {
      "type": "string",
      "ucp_request": { "create": "omit", "update": "required" }
    },
    "name": { "type": "string" }
  }
}
```

Resolve it for different operations:

```bash
# For create: id is removed from the schema
ucp-schema resolve schema.json --request --op create --pretty

# For update: id is required
ucp-schema resolve schema.json --request --op update --pretty
```

Validate a payload:

```bash
# This fails - id not allowed on create
echo '{"id": "123", "name": "test"}' > payload.json
ucp-schema validate payload.json --schema schema.json --request --op create

# This passes - id required on update
ucp-schema validate payload.json --schema schema.json --request --op update
```

## CLI Reference

### `resolve` - Generate operation-specific schema

```bash
ucp-schema resolve <schema> --request|--response --op <operation> [options]

Options:
  --pretty           Pretty-print JSON output
  --bundle           Inline all external $ref pointers (see Bundling)
  --strict=true      Reject unknown fields (default: false, see Validation)
  --output           Write to file instead of stdout
```

Examples:

```bash
# Resolve for create request, pretty print
ucp-schema resolve checkout.json --request --op create --pretty

# Resolve for read response
ucp-schema resolve checkout.json --response --op read

# Resolve from URL
ucp-schema resolve https://ucp.dev/schemas/checkout.json --request --op create

# Save resolved schema to file
ucp-schema resolve checkout.json --request --op create --output resolved.json
```

### `validate` - Validate payload against resolved schema

UCP payloads are self-describing: they embed capability metadata that declares which schemas apply. The validator can use this metadata directly, or you can specify an explicit schema.

```bash
# Self-describing mode (extracts schema from payload's ucp.capabilities)
ucp-schema validate <payload> --op <operation> [options]

# Explicit schema mode (overrides self-describing)
ucp-schema validate <payload> --schema <schema> --request|--response --op <operation> [options]

Options:
  --schema <path>              Explicit schema (overrides self-describing mode)
  --schema-local-base <dir>    Local directory to resolve schema URLs (see Validation Modes)
  --schema-remote-base <url>   URL prefix to strip when mapping to local (see URL Prefix Mapping)
  --request                    Direction is request (required with --schema, auto-detected otherwise)
  --response                   Direction is response (required with --schema, auto-detected otherwise)
  --json                       Output results as JSON (for automation)
  --strict=true                Reject unknown fields (default: false, see Validation)
```

Exit codes:
- `0` - Valid
- `1` - Validation failed (payload doesn't match schema)
- `2` - Schema error (invalid annotations, parse error, composition error)
- `3` - File/network error

### Validation Modes

The validator supports three modes based on which flags you provide:

| Mode | Command | Schema Source | Direction |
|------|---------|---------------|-----------|
| **Self-describing + remote** | `validate payload.json --op read` | `ucp.capabilities` URLs fetched | Auto-detected |
| **Self-describing + local** | `validate payload.json --schema-local-base ./dir --op read` | `ucp.capabilities` URLs mapped to local files | Auto-detected |
| **Explicit schema** | `validate payload.json --schema schema.json --request --op create` | Specified schema file/URL | Must specify `--request` or `--response` |

**Mode 1: Self-describing + remote fetch**

UCP payloads embed capability metadata declaring which schemas apply. The validator extracts schema URLs and fetches them:

```bash
# Payload has ucp.capabilities with schema URLs like https://ucp.dev/schemas/...
# Validator fetches schemas from those URLs and composes them
ucp-schema validate response.json --op read
```

Requires: payload has `ucp.capabilities` (responses) or `ucp.meta.profile` (requests).
Direction is auto-detected from payload structure.

**Mode 2: Self-describing + local resolution**

Same as above, but schema URLs are resolved to local files instead of fetched:

```bash
# Schema URL https://ucp.dev/schemas/shopping/checkout.json
# Maps to: ./local/schemas/shopping/checkout.json
ucp-schema validate response.json --schema-local-base ./local --op read
```

The `--schema-local-base` flag maps URL paths to local files:
- URL: `https://ucp.dev/schemas/shopping/checkout.json`
- Path extracted: `/schemas/shopping/checkout.json`
- Local file: `{schema-local-base}/schemas/shopping/checkout.json`

**URL Prefix Mapping**

When schema URLs have versioned prefixes that don't match your local directory structure, use `--schema-remote-base` to strip the prefix:

```bash
# Schema URL: https://ucp.dev/draft/schemas/shopping/checkout.json
# Local path: ./site/schemas/shopping/checkout.json (no "draft" directory)
ucp-schema validate response.json \
  --schema-local-base ./site \
  --schema-remote-base "https://ucp.dev/draft" \
  --op read
```

Mapping with `--schema-remote-base`:
- URL: `https://ucp.dev/draft/schemas/shopping/checkout.json`
- Strip prefix: `https://ucp.dev/draft``/schemas/shopping/checkout.json`
- Local file: `{schema-local-base}/schemas/shopping/checkout.json`

This is useful when published schemas have versioned `$id` URLs but your local files are organized without the version prefix.

Useful for: offline testing, local development, testing schema changes before deployment.

**Mode 3: Explicit schema**

Bypass self-describing metadata entirely by specifying `--schema`:

```bash
# Ignores any ucp.capabilities in payload, uses specified schema
ucp-schema validate order.json --schema checkout.json --request --op create

# Works with URLs too
ucp-schema validate order.json --schema https://ucp.dev/schemas/checkout.json --request --op create
```

Requires: explicit `--request` or `--response` flag (direction cannot be auto-detected).

**Error: No schema source**

If payload has no `ucp.capabilities`/`ucp.meta.profile` AND no `--schema` is specified:

```bash
ucp-schema validate payload.json --op read
# Error: payload is not self-describing: missing ucp.capabilities and ucp.meta.profile
```

**JSON output for automation:**

```bash
ucp-schema validate order.json --schema checkout.json --request --op create --json
# Output: {"valid":true}
# Or:     {"valid":false,"errors":[{"path":"","message":"..."}]}
```

### `lint` - Static analysis of schema files

Catch schema errors before runtime. The linter checks for issues that would cause failures during resolution or validation.

```bash
ucp-schema lint <path> [options]

Options:
  --format <text|json>  Output format (default: text)
  --strict              Treat warnings as errors
  --quiet, -q           Only show errors, suppress progress
```

**What it checks:**

| Category | Issue | Severity |
|----------|-------|----------|
| Syntax | Invalid JSON | Error |
| References | `$ref` to missing file | Error |
| References | `$ref` to missing anchor (e.g., `#/$defs/foo`) | Error |
| Annotations | Invalid `ucp_*` type (must be string or object) | Error |
| Annotations | Invalid visibility value (must be omit/required/optional) | Error |
| Hygiene | Missing `$id` field | Warning |
| Hygiene | Unknown operation in annotation (e.g., `{"delete": "omit"}`) | Warning |

**Examples:**

```bash
# Lint a directory of schemas
ucp-schema lint schemas/

# Lint single file, fail on warnings
ucp-schema lint checkout.json --strict

# CI-friendly JSON output
ucp-schema lint schemas/ --format json

# Quiet mode - only show errors
ucp-schema lint schemas/ --quiet
```

**Exit codes:**
- `0` - All files passed (or only warnings in non-strict mode)
- `1` - Errors found (or warnings in strict mode)
- `2` - Path not found

**JSON output format:**

```json
{
  "path": "schemas/",
  "files_checked": 5,
  "passed": 4,
  "failed": 1,
  "errors": 1,
  "warnings": 2,
  "results": [
    {
      "file": "checkout.json",
      "status": "error",
      "diagnostics": [
        {
          "severity": "error",
          "code": "E002",
          "path": "/properties/buyer/$ref",
          "message": "file not found: types/buyer.json"
        }
      ]
    }
  ]
}
```

## Schema Composition from Capabilities

UCP responses are self-describing - they embed `ucp.capabilities` declaring which schemas apply:

```json
{
  "ucp": {
    "capabilities": {
      "dev.ucp.shopping.checkout": [{
        "version": "2026-01-11",
        "schema": "https://ucp.dev/schemas/shopping/checkout.json"
      }],
      "dev.ucp.shopping.discount": [{
        "version": "2026-01-11",
        "schema": "https://ucp.dev/schemas/shopping/discount.json",
        "extends": "dev.ucp.shopping.checkout"
      }]
    }
  },
  "id": "...",
  "discounts": { ... }
}
```

**How composition works:**

1. **Root capability**: One capability has no `extends` - this is the base schema
2. **Extensions**: Capabilities with `extends` add fields to the root
3. **Composition**: Extensions define their additions in `$defs[root_capability_name]`
4. **allOf merge**: The composed schema uses `allOf` to combine all extensions

For the example above, the composed schema is:

```json
{
  "allOf": [
    { /* checkout's $defs["dev.ucp.shopping.checkout"] from discount.json */ }
  ]
}
```

**Schema authoring for extensions:**

Extension schemas must define their additions in `$defs` under the root capability name:

```json
{
  "$id": "https://ucp.dev/schemas/shopping/discount.json",
  "name": "dev.ucp.shopping.discount",
  "$defs": {
    "dev.ucp.shopping.checkout": {
      "allOf": [
        { "$ref": "checkout.json" },
        {
          "type": "object",
          "properties": {
            "discounts": { /* discount-specific fields */ }
          }
        }
      ]
    }
  }
}
```

**Graph validation:**

- Exactly one root capability (no `extends`)
- All `extends` references must exist in capabilities
- All extensions must transitively reach the root (no orphan extensions)

## Bundling External References

UCP schemas often use `$ref` to reference external files:

```json
{
  "properties": {
    "buyer": { "$ref": "types/buyer.json" },
    "shipping": { "$ref": "types/address.json#/$defs/postal" }
  }
}
```

The `--bundle` flag inlines all external references, producing a self-contained schema:

```bash
ucp-schema resolve checkout.json --request --op create --bundle --pretty
```

**When to use bundling:**
- Distributing schemas without file dependencies
- Feeding schemas to tools that don't support external refs
- Debugging to see the fully-expanded schema
- Pre-processing for faster repeated validation

**How it works:**
- External file refs (`"$ref": "types/buyer.json"`) are loaded and inlined
- Fragment refs (`"$ref": "types/common.json#/$defs/address"`) navigate to the specific definition
- Internal refs within external files (`"$ref": "#/$defs/foo"`) resolve correctly against their source file
- Self-referential recursive types (`"$ref": "#"`) are preserved (can't be inlined)
- Circular references between files are detected and reported as errors

## Validation

By default, the validator respects UCP's extensibility model:

- **Validates:** Payload conforms to spec shape (types, required fields, enums, nested structures)
- **Allows:** Additional/unknown fields (extensibility is intentional)

```bash
# Validates that known fields are correct, allows extra fields
ucp-schema validate response.json --op read
```

This works because UCP schemas use `additionalProperties: true` intentionally - extensions add new fields, and forward compatibility requires tolerating unknown fields.

**Enabling strict mode:**

For cases where you want to reject unknown fields (e.g., closed systems, catching typos):

```bash
# Reject any fields not defined in schema
ucp-schema validate payload.json --schema schema.json --request --op create --strict=true

# Resolved schema will have additionalProperties: false injected
ucp-schema resolve schema.json --request --op create --strict=true
```

**What strict mode does:**
- Adds `additionalProperties: false` to all object schemas (root, nested, in arrays, in definitions)
- Only injects `false` when `additionalProperties` is missing or explicitly `true`
- Preserves custom `additionalProperties` schemas (e.g., `{"type": "string"}`)
- Preserves explicit `additionalProperties: false`

**Note:** Strict mode does not work well with `allOf` composition (each branch validates independently and rejects properties from other branches). Use default non-strict mode for composed schemas.

## Visibility Rules

Annotations control how fields appear in the resolved schema:

| Value           | Effect on Properties | Effect on Required Array |
| --------------- | -------------------- | ------------------------ |
| `"omit"`        | Field removed        | Field removed            |
| `"required"`    | Field kept           | Field added              |
| `"optional"`    | Field kept           | Field removed            |
| (no annotation) | Field kept           | Unchanged                |

### Annotation Formats

**Shorthand** - applies to all operations:
```json
{ "ucp_request": "omit" }
```

**Per-operation** - different behavior per operation:
```json
{ "ucp_request": { "create": "omit", "update": "required", "read": "omit" } }
```

**Separate request/response:**
```json
{
  "ucp_request": { "create": "omit" },
  "ucp_response": "required"
}
```

## More Information

See **[FAQ.md](./FAQ.md)** for common questions about validator behavior and design decisions

## License

MIT