Metadata Plugin
Metadata Plugin protocol schemas
Metadata Plugin Protocol
Defines the specification for the Metadata Plugin — the central authority responsible for managing ALL metadata across the ObjectStack platform.
Architecture
The Metadata Plugin consolidates all scattered metadata operations into a single, cohesive plugin that "takes over" the entire platform's metadata management:
┌──────────────────────────────────────────────────────────────────┐
│ Metadata Plugin │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────────┐ │
│ │ Type Registry │ │ Loader │ │ Customization Layer │ │
│ │ (all types) │ │ (file/db/s3)│ │ (overlay / merge) │ │
│ └──────────────┘ └──────────────┘ └──────────────────────┘ │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────────┐ │
│ │ Persistence │ │ Query │ │ Lifecycle │ │
│ │ (db records) │ │ (search) │ │ (validate/deploy) │ │
│ └──────────────┘ └──────────────┘ └──────────────────────┘ │
└──────────────────────────────────────────────────────────────────┘Alignment
- Salesforce: Metadata API (deploy, retrieve, describe)
- ServiceNow: System Dictionary + Metadata API
- Kubernetes: API Server + CRD Registry
References
- kernel/metadata-loader.zod.ts — MetadataManager wiring (datasource, cache, write gates)
- system/metadata-persistence.zod.ts — Database record format + loader/watch envelope types
- contracts/metadata-service.ts — Service interface
Source: packages/spec/src/kernel/metadata-plugin.zod.ts
TypeScript Usage
import { MetadataBulkResultSchema, MetadataDependencySchema, MetadataPluginConfigSchema, MetadataPluginManifestSchema, MetadataQuerySchema, MetadataQueryResultSchema, MetadataTypeSchema, MetadataTypeRegistryEntrySchema, MetadataValidationResultSchema } from '@objectstack/spec/kernel';
import type { MetadataBulkResult, MetadataDependency, MetadataPluginConfig, MetadataPluginManifest, MetadataQuery, MetadataQueryResult, MetadataType, MetadataTypeRegistryEntry, MetadataValidationResult } from '@objectstack/spec/kernel';
// Validate data
const result = MetadataBulkResultSchema.parse(data);MetadataBulkResult
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| total | integer | ✅ | Total items processed |
| succeeded | integer | ✅ | Successfully processed |
| failed | integer | ✅ | Failed items |
| errors | { type: string; name: string; error: string }[] | optional | Per-item errors |
Nested Shape: MetadataBulkResult.errors[number]
| Property | Type | Required | Description |
|---|---|---|---|
| type | string | ✅ | Metadata type |
| name | string | ✅ | Item name |
| error | string | ✅ | Error message |
MetadataDependency
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| sourceType | string | ✅ | Dependent metadata type |
| sourceName | string | ✅ | Dependent metadata name |
| targetType | string | ✅ | Referenced metadata type |
| targetName | string | ✅ | Referenced metadata name |
| kind | Enum<'reference' | 'extends' | 'includes' | 'triggers'> | ✅ | How the dependency is formed |
MetadataPluginConfig
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| storage | { datasource?: string; tableName: string; fallback: Enum<'filesystem' | 'memory' | 'none'>; rootDir?: string; … } | ✅ | Storage backend configuration |
| customizationPolicies | never | optional | [REMOVED] config.customizationPolicies was removed from MetadataPluginConfig in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — it never had an effect: no code ever read a customization policy, and the overlay protocol it configured was itself unreachable from any served surface (ADR-0126 supersedes it on the record). Delete the key. What a customization may touch is governed by the real mechanisms: ADR-0005's org-scoped overlay (opt-in via allowOrgOverride on DEFAULT_METADATA_TYPE_REGISTRY, enforced at the REST meta write doors) and ADR-0126's packaged-metadata model (clone + ledger disable). |
| mergeStrategy | never | optional | [REMOVED] config.mergeStrategy was removed from MetadataPluginConfig in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — it never had an effect: no 3-way merge engine ever existed to read it, and package upgrades do not merge customizations (ADR-0126: upgrades rewrite the packaged base; customer choices live in the ledger and are never merged). Delete the key. There is no replacement — upgrade-vs-customization separation is the model, not a configurable strategy. |
| additionalTypes | never | optional | [REMOVED] config.additionalTypes was removed from MetadataPluginConfig in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — it never had an effect: the only production writer of the metadata type registry is setTypeRegistry(DEFAULT_METADATA_TYPE_REGISTRY), which replaces the array outright, so nothing ever merged these entries and the live type set was exactly the built-in registry whatever you declared here. Delete the key. There is no declared-kind channel: a kind enters the live metadata-type set as a side effect of registering an ITEM of that kind (SchemaRegistry.registerItem during app/manifest registration, or MetadataManager.register at runtime); bind its schema with registerMetadataTypeSchema(type, schema) from your plugin's init(ctx) so GET /api/v1/meta serves a real JSON Schema for it. |
| enableEvents | boolean | optional (default: true) | Emit metadata change events |
| validateOnWrite | boolean | optional (default: true) | Validate metadata on write |
| enableVersioning | boolean | optional (default: false) | Track metadata version history |
| cacheMaxItems | integer | optional (default: 10000) | Max items in memory cache |
| bootstrap | Enum<'eager' | 'lazy' | 'artifact-only'> | optional (default: "eager") | How metadata is primed at plugin start (eager / lazy / artifact-only) |
Nested Shape: MetadataPluginConfig.storage
| Property | Type | Required | Description |
|---|---|---|---|
| datasource | string | optional | Datasource name reference for database persistence |
| tableName | string | optional (default: "sys_metadata") | Database table name for metadata storage |
| fallback | Enum<'filesystem' | 'memory' | 'none'> | optional (default: "none") | Fallback strategy when datasource is unavailable |
| rootDir | string | optional | Root directory path |
| formats | Enum<'yaml' | 'json' | 'typescript' | 'javascript'>[] | optional (default: ["typescript","json","yaml"]) | Enabled formats |
| cache | { databaseLoader?: object } | optional | Cache settings — only databaseLoader is read at runtime; the outer keys are retired |
| watch | boolean | optional (default: false) | Enable file watching |
| watchOptions | { ignored?: string[]; persistent: boolean; ignoreInitial: boolean } | optional | File watcher options |
| validation | { strict: boolean; throwOnError: boolean } | optional | Validation settings |
| loaderOptions | Record<string, any> | optional | Loader-specific configuration |
| persistence | { writable: boolean } | optional | Persistence write gates |
MetadataPluginManifest
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| id | 'com.objectstack.metadata' | ✅ | Metadata plugin ID |
| name | 'ObjectStack Metadata Service' | ✅ | Plugin name |
| version | string | ✅ | Plugin version |
| type | 'standard' | ✅ | Plugin type |
| description | string | optional (default: "Core metadata management service for ObjectStack platform") | Plugin description |
| capabilities | { crud: boolean; query: boolean; overlay: boolean; watch: boolean; … } | ✅ | Plugin capabilities |
| config | { storage: object; enableEvents: boolean; validateOnWrite: boolean; enableVersioning: boolean; … } | optional | Plugin configuration |
Nested Shape: MetadataPluginManifest.capabilities
| Property | Type | Required | Description |
|---|---|---|---|
| crud | boolean | optional (default: true) | Supports metadata CRUD |
| query | boolean | optional (default: true) | Supports metadata query |
| overlay | boolean | optional (default: true) | Supports customization overlays |
| watch | boolean | optional (default: false) | Supports file watching |
| importExport | boolean | optional (default: true) | Supports import/export |
| validation | boolean | optional (default: true) | Supports schema validation |
| versioning | boolean | optional (default: false) | Supports version history |
| events | boolean | optional (default: true) | Emits metadata events |
Nested Shape: MetadataPluginManifest.config
| Property | Type | Required | Description |
|---|---|---|---|
| storage | { datasource?: string; tableName: string; fallback: Enum<'filesystem' | 'memory' | 'none'>; rootDir?: string; … } | ✅ | Storage backend configuration |
| customizationPolicies | never | optional | [REMOVED] config.customizationPolicies was removed from MetadataPluginConfig in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — it never had an effect: no code ever read a customization policy, and the overlay protocol it configured was itself unreachable from any served surface (ADR-0126 supersedes it on the record). Delete the key. What a customization may touch is governed by the real mechanisms: ADR-0005's org-scoped overlay (opt-in via allowOrgOverride on DEFAULT_METADATA_TYPE_REGISTRY, enforced at the REST meta write doors) and ADR-0126's packaged-metadata model (clone + ledger disable). |
| mergeStrategy | never | optional | [REMOVED] config.mergeStrategy was removed from MetadataPluginConfig in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — it never had an effect: no 3-way merge engine ever existed to read it, and package upgrades do not merge customizations (ADR-0126: upgrades rewrite the packaged base; customer choices live in the ledger and are never merged). Delete the key. There is no replacement — upgrade-vs-customization separation is the model, not a configurable strategy. |
| additionalTypes | never | optional | [REMOVED] config.additionalTypes was removed from MetadataPluginConfig in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — it never had an effect: the only production writer of the metadata type registry is setTypeRegistry(DEFAULT_METADATA_TYPE_REGISTRY), which replaces the array outright, so nothing ever merged these entries and the live type set was exactly the built-in registry whatever you declared here. Delete the key. There is no declared-kind channel: a kind enters the live metadata-type set as a side effect of registering an ITEM of that kind (SchemaRegistry.registerItem during app/manifest registration, or MetadataManager.register at runtime); bind its schema with registerMetadataTypeSchema(type, schema) from your plugin's init(ctx) so GET /api/v1/meta serves a real JSON Schema for it. |
| enableEvents | boolean | optional (default: true) | Emit metadata change events |
| validateOnWrite | boolean | optional (default: true) | Validate metadata on write |
| enableVersioning | boolean | optional (default: false) | Track metadata version history |
| cacheMaxItems | integer | optional (default: 10000) | Max items in memory cache |
| bootstrap | Enum<'eager' | 'lazy' | 'artifact-only'> | optional (default: "eager") | How metadata is primed at plugin start (eager / lazy / artifact-only) |
MetadataQuery
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| types | Enum<'object' | 'field' | 'hook' | 'seed' | 'mapping' | 'view' | 'page' | 'dashboard' | 'app' | 'action' | 'report' | 'dataset' | 'flow' | 'job' | 'datasource' | 'external_catalog' | 'translation' | 'api' | 'email_template' | 'doc' | 'book' | 'permission' | 'position' | 'capability' | 'agent' | 'tool' | 'skill'>[] | optional | Filter by metadata types |
| namespaces | string[] | optional | Filter by namespaces |
| packageId | string | optional | Filter by owning package |
| search | string | optional | Full-text search query |
| scope | Enum<'system' | 'platform' | 'user'> | optional | Filter by scope |
| state | Enum<'draft' | 'active' | 'archived' | 'deprecated'> | optional | Filter by lifecycle state |
| tags | string[] | optional | Filter by tags |
| sortBy | Enum<'name' | 'type' | 'updatedAt' | 'createdAt'> | optional (default: "name") | Sort field |
| sortOrder | Enum<'asc' | 'desc'> | optional (default: "asc") | Sort direction |
| page | integer | optional (default: 1) | Page number |
| pageSize | integer | optional (default: 50) | Items per page |
MetadataQueryResult
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| items | { type: string; name: string; namespace?: string; label?: string; … }[] | ✅ | Matched metadata items |
| total | integer | ✅ | Total matching items |
| page | integer | ✅ | Current page |
| pageSize | integer | ✅ | Page size |
Nested Shape: MetadataQueryResult.items[number]
| Property | Type | Required | Description |
|---|---|---|---|
| type | string | ✅ | Metadata type |
| name | string | ✅ | Item name |
| namespace | string | optional | Namespace |
| label | string | optional | Display label |
| scope | Enum<'system' | 'platform' | 'user'> | optional | |
| state | Enum<'draft' | 'active' | 'archived' | 'deprecated'> | optional | |
| packageId | string | optional | |
| updatedAt | string | optional |
MetadataType
Allowed Values
objectfieldhookseedmappingviewpagedashboardappactionreportdatasetflowjobdatasourceexternal_catalogtranslationapiemail_templatedocbookpermissionpositioncapabilityagenttoolskill
MetadataTypeRegistryEntry
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| type | Enum<'object' | 'field' | 'hook' | 'seed' | 'mapping' | 'view' | 'page' | 'dashboard' | 'app' | 'action' | 'report' | 'dataset' | 'flow' | 'job' | 'datasource' | … +12 more> | ✅ | Metadata type identifier |
| label | string | ✅ | Display label for the metadata type |
| description | string | optional | Description of the metadata type |
| filePatterns | string[] | ✅ | Glob patterns to discover files of this type |
| supportsOverlay | boolean | optional (default: true) | Whether overlay customization is supported |
| allowOrgOverride | boolean | optional (default: false) | Allow per-org overlay writes via runtime metadata API |
| allowRuntimeCreate | boolean | optional (default: true) | Allow runtime creation via API |
| supportsVersioning | boolean | optional (default: false) | Whether version history is tracked |
| executionPinned | boolean | optional (default: false) | Transaction rows reference a specific version_hash; history GC is disabled and getByHash() MUST resolve old hashes (ADR-0009) |
| loadOrder | integer | optional (default: 100) | Loading priority (lower = earlier) |
| domain | Enum<'data' | 'ui' | 'automation' | 'system' | 'security' | 'ai'> | ✅ | Protocol domain |
| actions | { name: string; label: string | Record<string, string>; description?: string | Record<string, string>; objectName?: string; … }[] | optional | Declarative type-level actions (e.g. datasource "Test connection"), reusing ActionSchema; merged with plugin-registered actions when emitted |
Allowed Values: MetadataTypeRegistryEntry.type
objectfieldhookseedmappingviewpagedashboardappactionreportdatasetflowjobdatasourceexternal_catalogtranslationapiemail_templatedocbookpermissionpositioncapabilityagenttoolskill
Nested Shape: MetadataTypeRegistryEntry.actions[number]
| Property | Type | Required | Description |
|---|---|---|---|
| name | string | ✅ | Machine name (lowercase snake_case) |
| label | string | Record<string, string> | ✅ | Display label |
| description | string | Record<string, string> | optional | Explanatory line shown under the title in the action's param dialog. Carries the confirm question for an action that collects params (one dialog, not two —). Not the LLM-facing ai.description. |
| objectName | string | optional | Target object this action belongs to. When set, the action is auto-merged into the object's actions array by defineStack(). |
| icon | string | optional | Icon name |
| locations | Enum<'list_toolbar' | 'list_item' | 'record_header' | 'record_more' | …>[] | optional | Locations where this action is visible |
| component | Enum<'action:button' | 'action:icon' | 'action:menu' | 'action:group'> | optional | Visual component override |
| type | Enum<'script' | 'url' | 'modal' | 'flow' | 'api' | 'form'> | optional (default: "script") | Action functionality type — the dispatch route. The declarative single-record field write is not a type: it is operation: 'update' + patch on the default script route. |
| target | string | optional | URL, Script Name, Flow ID, or API Endpoint. Supports ${param.X} and ${ctx.X} interpolation. |
| openIn | Enum<'self' | 'new-tab'> | optional | For type:'url' — where to open target. 'new-tab' opens a new browser tab; 'self' navigates in place. When omitted, external/absolute URLs open in a new tab and relative URLs navigate in place. Static execution option — keep it OUT of params (which is user-input-collection only). |
| body | { language: 'expression'; source: string } | { language: 'js'; source: string; capabilities?: Enum<'api.read' | 'api.write' | 'api.transaction' | 'crypto.uuid' | 'log'>[]; timeoutMs?: integer; … } | optional | Action body — expression (L1) or sandboxed JS (L2). Only used when type is script. |
| operation | Enum<'update'> | optional | The declarative single-record field write, mirroring a list view's bulkActionDefs: 'update' applies patch (merged under the collected params) to the current record on the data plane AS THE CALLER — never system-elevated — so the caller's permissions, the object's hooks and its validations fire as for a user edit. type stays at its default 'script' (the platform action route the write is performed on); target/body/method/bodyExtra are refused beside it. 'delete' and 'custom' have no row-level form. |
| patch | Record<string, any> | optional | For operation: 'update' — static field values written to the current record, merged UNDER the user-supplied params so a fixed value can be declared without exposing it in the dialog. Written on the data plane as the caller: object permissions, hooks and validations fire as for a user edit. Refused on an action without operation: 'update' (it would be silently dropped). |
| execution | Enum<'perRecord' | 'aggregate'> | optional | The bulk dispatch contract this action's BODY is written for, in bulkActionDefs' own vocabulary: 'perRecord' = one dispatch per selected row carrying that row's recordId (the view's bulkActions: ['<name>'] bare-string form); 'aggregate' = ONE dispatch for the whole selection carrying every id in params._selectedIds (a bulkActionDefs entry with execution: 'aggregate'). Optional with NO default — omit it only when the body genuinely serves both. A list view wiring a declared action under the other contract is refused by @objectstack/lint (action-dispatch-contract-mismatch). |
| execute | never | optional | [REMOVED] execute was removed in @objectstack/spec 17 — use target. Rename the key; the value (a handler / flow / URL ref) is unchanged. Run os migrate meta --from 16 to list the mechanical edits for existing sources; apply them by hand. |
| params | { name?: string; field?: string; objectOverride?: string; label?: string | Record<string, string>; … }[] | optional | Input parameters required from user — an ActionParam[] DEFINITION array, never a payload map (a static request body goes in bodyExtra). |
| variant | Enum<'primary' | 'secondary' | 'danger' | 'ghost' | 'link'> | optional | Button visual variant for styling (primary = highlighted, danger = destructive, ghost = transparent) |
| order | number | optional | Sort order within a location group (lower = higher). Promotes/demotes an action toward the record_header primary button; stable, so actions without order keep their registration order. |
| confirmText | string | Record<string, string> | optional | Confirmation message before execution. On a registered action, pairing this with a non-empty params is refused — that opens a second dialog for one decision; put the question on description instead. Correct on a param-LESS action, where the confirm is the only dialog there is. |
| successMessage | string | Record<string, string> | optional | Success message to show after execution |
| errorMessage | string | Record<string, string> | optional | Error message to show when the action fails (overrides the raw error). |
| refreshAfter | boolean | optional (default: false) | Refresh view after execution |
| undoable | boolean | optional | Offer an Undo affordance after this single-record update action succeeds. operation: 'update' is the declared form of that action — its patch names exactly the fields whose prior values are captured. |
| resultDialog | { title?: string | Record<string, string>; description?: string | Record<string, string>; acknowledge?: string | Record<string, string>; format?: Enum<'qrcode' | 'code-list' | 'secret' | 'text' | 'json'>; … } | optional | Render API response in a one-shot reveal dialog (suppresses successMessage when set). |
| visible | boolean | string | { dialect: Enum<'cel' | 'cron' | 'template'>; source: string; ast?: any; meta?: object } | optional | Visibility predicate — true/false literal, CEL string, or {dialect, source} envelope. The action is offered when it evaluates TRUE. Omit = always visible. |
| requiresFeature | Enum<'twoFactor' | 'organization' | 'multiOrgEnabled' | 'degradedTenancy' | …> | optional | Public auth feature flag gating this action; lowered into visible at parse time. |
| disabled | boolean | string | { dialect: Enum<'cel' | 'cron' | 'template'>; source: string; ast?: any; meta?: object } | optional | Disabled predicate — true/false literal, CEL string, or {dialect, source} envelope. The action is shown but refused when it evaluates TRUE. Omit = never disabled. |
| requiredPermissions | string[] | optional | [ADR-0066 D4] Capabilities required to invoke this action. Enforced with 403 on the platform action route (script/flow/modal + MCP) and mirrored as a UI hide; a type: api action pointed at a custom endpoint must re-check it there. |
| shortcut | never | optional | [REMOVED] action.shortcut was removed in @objectstack/spec 17.0.0 (audit close-out) — it never triggered anything: no keydown listener feeds ActionEngine.getShortcuts(), and objectui's keyboard stack (useKeyboardShortcuts) is hand-registered and never consults action metadata. Delete the key. For a real shortcut, register the key in the Console keyboard stack and have its handler invoke the action by name. Run os migrate meta --from 16 to list the mechanical edits for existing sources; apply them by hand. |
| bulkEnabled | never | optional | [REMOVED] action.bulkEnabled was removed in @objectstack/spec 17.0.0 (audit close-out) — the multi-select toolbar is driven by the LIST VIEW's bulkActions / bulkActionDefs, never by this flag, so setting it changed nothing. Delete the key and declare the action in the view's bulkActions instead. Run os migrate meta --from 16 to list the mechanical edits for existing sources; apply them by hand. |
| ai | { exposed?: boolean; description?: string; category?: Enum<'data' | 'action' | 'flow' | 'integration' | 'vector_search' | 'analytics' | 'utility'>; paramHints?: Record<string, object>; … } | optional | AI exposure (opt-in). Set ai.exposed=true + ai.description to make this callable by agents. |
| recordIdParam | string | optional | Body key to inject the row id into when running from a list_item context. |
| recordIdField | string | optional | Row field whose value seeds recordIdParam. Defaults to "id". |
| bodyShape | 'flat' | { wrap: string } | optional | Body wrapping: flat (default) or { wrap: key } to nest user-collected params under a key. |
| method | Enum<'POST' | 'PATCH' | 'PUT' | 'DELETE'> | optional | HTTP method for type:"api" actions. Defaults to POST. |
| bodyExtra | Record<string, any> | optional | Static request-body fields for a type:"api" action, merged last (overrides user params). {{page.<var>}} tokens are resolved by the runtime. This — not params — is where a payload goes. |
| mode | Enum<'create' | 'edit' | 'delete' | 'custom'> | optional | Semantic mode of the action. |
| opensInNewTab | boolean | optional | Open the action result in a new tab. The renderer pre-opens the tab synchronously on click (popup-blocker-safe) and navigates it to the handler's redirectUrl. |
| newTabUrl | string | optional | Direct new-tab URL template ({recordId} placeholder). When set with opensInNewTab, the renderer navigates the pre-opened tab here immediately — no action POST. The endpoint must enforce auth itself. |
| onSuccess | { navigate: string; openIn?: Enum<'self' | 'newTab'> } | optional | Post-success navigation for type:'api' and type:'script' actions. navigate is a route/URL template interpolating ${param.*}, ${ctx.*} and ${result.*} (the server response); openIn defaults 'self'. The handler-return convention ({ redirectUrl } without openIn) keeps its 17.0.0 new-tab behavior. |
| aria | { ariaLabel?: string | Record<string, string>; ariaDescribedBy?: string; role?: string } | optional | ARIA accessibility attributes |
| _lock | Enum<'none' | 'no-overlay' | 'no-delete' | 'full'> | optional | Item-level lock — controls overlay & delete (ADR-0010). |
| _lockReason | string | optional | Human-readable reason shown when a write is refused by _lock. |
| _lockSource | Enum<'artifact' | 'package' | 'env-forced'> | optional | Layer that set _lock (artifact | package | env-forced). |
| _provenance | Enum<'package' | 'org' | 'env-forced'> | optional | Origin of the item (package | org | env-forced). |
| _packageId | string | optional | Owning package machine id. |
| _packageVersion | string | optional | Owning package version. |
| _lockDocsUrl | string | optional | Optional documentation link surfaced next to _lockReason. |
MetadataValidationResult
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| valid | boolean | ✅ | Whether the metadata is valid |
| errors | { path: string; message: string; code?: string }[] | optional | Validation errors |
| warnings | { path: string; message: string }[] | optional | Validation warnings |
Nested Shape: MetadataValidationResult.errors[number]
| Property | Type | Required | Description |
|---|---|---|---|
| path | string | ✅ | JSON path to the invalid field |
| message | string | ✅ | Error description |
| code | string | optional | Error code |
Nested Shape: MetadataValidationResult.warnings[number]
| Property | Type | Required | Description |
|---|---|---|---|
| path | string | ✅ | JSON path to the field |
| message | string | ✅ | Warning description |