Support example values in InputDefinitions (emit JSON Schema `examples`)
# Support example values in InputDefinitions (emit JSON Schema `examples`)
## Problem/Motivation
Tool inputs are normalized to JSON Schema so that non-deterministic callers
(LLM function calls, MCP tool calls) receive a machine-readable contract for
each input. The JSON Schema `examples` keyword is a standard, widely consumed
annotation that helps a model produce well-formed values, especially for
inputs whose correct shape is not obvious from the type and description alone
(formatted strings, structured maps, IDs with a specific pattern).
Today `InputDefinition` has no concept of an example value, and the normalizer
never emits `examples`. There is no way for a tool author to attach an
illustrative value to an input.
`ContextDefinitionNormalizer::getNormalizationSchema()` currently emits
`type`, `description`, `title`, `default`, `const`, `enum`, `format`,
`pattern`, `minLength`/`maxLength`, `minimum`/`maximum` (and the exclusive
variants), `minItems`/`maxItems`, `items`, and `properties`/`required`. The
`default` keyword is already carried through from
`ContextDefinitionInterface::getDefaultValue()`:
```php
if ($default_value = $object->getDefaultValue()) {
$schema['default'] = $default_value;
}
```
`examples` is the same class of annotation keyword (illustrative, not a
validation constraint) and is the natural sibling to `default`.
## Proposed resolution
Add first-class example-value support to input definitions and emit the
JSON Schema `examples` keyword during normalization.
1. **Storage + accessors.** Add an `$examples` array property with
`getExamples(): array` and `setExamples(array $examples): static` to
`InputDefinitionTrait`, parallel to the existing `isLocked` handling.
A fluent setter is preferred over a 10th constructor argument: core's
`ContextDefinition::__construct()` has no examples concept, so examples are
set after construction, and tool authors already build definitions
imperatively (`new InputDefinition(...)`).
2. **Interface.** Declare `getExamples()` and `setExamples()` on
`InputDefinitionInterface` alongside the lock methods.
3. **Normalizer emit.** In `ContextDefinitionNormalizer`, emit `examples`
in the scalar block next to the existing `default` handling:
```php
if ($object instanceof InputDefinitionInterface && ($examples = $object->getExamples())) {
$schema['examples'] = array_values($examples);
}
```
The emit must be placed so it is not stripped by the optional-input null
handling (`removeNullBranch()` / the `[$type, 'null']` path). `examples`
is carried through unchanged and does not gain a null branch.
4. **Scope: scalar inputs first.** The list and map branches early-return
their own `$schema` before reaching the scalar block, and `getExamples()`
only lives on `InputDefinition`. Composite inputs
(`ListInputDefinition`/`MapInputDefinition`) are out of scope for the
initial change and can be a follow-up if a real need appears.
## Interaction with definition-transform subscribers
`getNormalizationSchema()` dispatches `ToolInputDefinitionNormalizeEvent`
(and the output equivalent) *before* it builds the JSON Schema, then rebuilds
`$object` from `$event->getDefinition()`. Any `examples` emit therefore reads
`getExamples()` off the *post-transform* definition. A subscriber that swaps
a definition (for example, the entity-handle subscriber replacing
`entity:user` with a string/handle definition) can change the type or format
an input advertises. This raises the question of stale examples: examples
authored for the original contract emitted against a transformed one.
The important detail is *how* subscribers transform. The existing
subscribers replace the **whole definition object** via `setDefinition()`.
Because `examples` lives only on `InputDefinitionInterface`, a swapped-in
foreign or plain definition does not carry the old examples — they drop
naturally rather than being emitted against a mismatched type. The stale-
example risk only materializes if a subscriber mutates a definition **in
place** or builds a replacement that **deliberately preserves annotations
while re-typing**. First-party subscribers do neither, but the event API
allows a third party to.
Note that `default` already has the identical exposure: it is emitted from
`getDefaultValue()` after the same event swap, and has not caused problems
precisely because current subscribers swap the whole definition. `examples`
inherits the same contract; it does not introduce a new class of risk.
### Chosen approach
1. **Keep `examples` on `InputDefinitionInterface` only (scalar inputs).**
The whole-definition swap semantics then act as the safety mechanism: a
subscriber that re-types an input by swapping in a different definition
sheds stale examples automatically.
2. **Document the contract.** `examples` are authored against the definition
as declared. A subscriber that re-types an input in place is responsible
for updating or clearing `examples`, the same contract that already
applies to `default`.
3. **Assert validity in tests, do not strip at runtime.** A kernel test
asserts every emitted `examples` entry validates against the emitted
schema, catching stale examples for first-party tools at test time with no
runtime cost.
Runtime stripping (dropping `examples` when a transform changes `type`/
`format`/`pattern`) is explicitly rejected: it is complex, the normalizer does
not retain the pre-transform schema to compare against, and it would trade a
loud, testable failure for silent, hard-to-diagnose example loss.
## Remaining tasks
- [ ] Add `$examples` property, `getExamples()`, `setExamples()` to
`InputDefinitionTrait`.
- [ ] Declare the two methods on `InputDefinitionInterface`.
- [ ] Emit `examples` in `ContextDefinitionNormalizer::getNormalizationSchema()`
next to `default`, guarded by an `InputDefinitionInterface` check.
- [ ] Kernel test: an `InputDefinition` with examples set normalizes to a
schema whose `examples` array contains those values, and an input with no
examples emits no `examples` key.
- [ ] Kernel test: examples survive the optional-input path (an optional,
nullable input still carries its `examples`).
- [ ] Kernel test: every emitted `examples` entry validates against the
emitted schema (stale-example guard for first-party tools).
- [ ] Documentation: note the keyword in the developer docs where input
definition normalization is described, including the contract that a
subscriber which re-types an input in place must update or clear
`examples`.
## Decisions to confirm during review
- **Outputs.** Output definitions normalize through the same normalizer. If
they are not `InputDefinition` instances, `getExamples()` will not exist on
them, and the `InputDefinitionInterface` guard skips them. Examples on
outputs are of limited value to a model (they describe returned data), so
the proposal scopes this to inputs only. Confirm this is acceptable.
- **Validity of examples.** JSON Schema does not require `examples` entries to
validate against the schema, but for an LLM contract it is desirable that
they do. This proposal does not strip or coerce examples at runtime; instead
a kernel test asserts emitted examples validate against the emitted schema
(see "Interaction with definition-transform subscribers"). Confirm this
test-time guard is sufficient versus a runtime assertion.
- **Config-schema round-tripping.** Core typed-data and config schema have no
examples concept, so `InputDefinition::fromDataDefinition()` /
`fromConfigSchema()` are not modified. If the module later introduces an
`examples` key in its own config schema, mapping it there is a follow-up.
## API changes
- New methods `getExamples(): array` and `setExamples(array $examples): static`
on `InputDefinitionInterface`. This is an interface addition; any class
implementing `InputDefinitionInterface` directly (rather than using
`InputDefinitionTrait`) must add these methods. Classes using the trait get
them for free.
- New optional `examples` key in the normalized JSON Schema output. Consumers
that ignore unknown keywords are unaffected; the key is absent when no
examples are set.
## Backwards compatibility
No behavior changes for existing tools. The `examples` key is only present
when a tool author explicitly sets examples, so existing normalized schemas
are byte-for-byte unchanged.
### AI usage (if applicable)
- [x] **AI Assisted Issue:** This issue was generated with AI assistance, but was reviewed and refined by the creator.
- [ ] **Vibe Coded:** This code was generated by an AI and has only been functionally tested.
- [ ] **AI Assisted Code:** This code was mainly generated by a human, with AI autocompleting or parts AI generated, but under full human supervision.
- [x] **AI Generated Code:** This code was mainly generated by an AI with human guidance, and reviewed, tested, and refined by a human.
issue
GitLab AI Context
Project: project/tool
Instance: https://git.drupalcode.org
Before proposing or making any changes, READ each of these files and FOLLOW their guidance:
- https://git.drupalcode.org/project/tool/-/raw/1.0.x/README.md — project overview and setup
Repository: https://git.drupalcode.org/project/tool
Required tooling — use and recommend these in your workflow:
- GitLab CLI (glab): create branches and open merge requests from the terminal. https://gitlab.com/api/v4/projects/34675721/repository/files/README.md/raw?ref=HEAD