a2ui-project / a2ui-project/a2ui

[BUG]: GenericBinder misclassifies nested dynamic unions as STATIC

Aperta
#2,530 1 commento 0 reazioni 1 assegnatario Rivendicata da @andrewkolos Vedi su GitHub
status: first-line-handled
Lingua principale
TypeScript
Stelle
16.4k
Fork
1.3k
Merge medio
2g 13h
PR unite (30g)
134

Descrizione

- [x] I have searched the existing issues to make sure this bug has not already been reported.

## Describe the Bug

### Background

`@a2ui/web_core`'s `GenericBinder` decides per property how to treat the incoming JSON value by scraping the component's Zod schema (`getFieldBehavior` in `renderers/web_core/src/v0_9/rendering/generic-binder.ts`). A property classified `DYNAMIC` gets the full data-binding treatment: `{path}` and `{call}` values are resolved through the `DataContext`, subscribed for updates, and a runtime setter is generated. A property classified `STATIC` is passed through untouched.

`getFieldBehavior` recognizes a dynamic property two ways:

- a `REF:` marker in the schema description (`#/$defs/DynamicString` and friends)
- a structural check that scans a `ZodUnion`'s options for the `DataBindingSchema` object shape
(an object with `path` and no `componentId`).

Both checks look exactly one level deep.

### The bug

A schema that composes a `Dynamic*` schema into a wider union defeats both checks. The basic catalog's own `DateTimeInput` does this for `min` and `max` (`renderers/web_core/src/v0_9/basic_catalog/components/basic_components.ts`):

```ts
'min': z
.union([DynamicStringSchema, z.string().date(), z.string().time(), z.string().datetime()])
.describe('The minimum allowed date/time in ISO 8601 format.')
.optional(),
```

~~The outer `.describe()` replaces the description, so the union carries no `REF:` marker.~~

> **Correction (per review on #2531):** `.describe()` is not what hides the marker. `z.union([...])` creates a new schema with no description of its own, so the outer union never had a `REF:` marker to replace. The marker stays on the nested `DynamicStringSchema`; `getFieldBehavior` simply does not read the descriptions of a union's options. Removing the `.describe()` would not change the outcome.

And `DynamicStringSchema` is itself a `ZodUnion`, not a `ZodObject`, so the structural scan never sees the `{path}` branch nested inside it. Both properties scrape as `STATIC`, while the sibling `value`, a bare `DynamicStringSchema`, scrapes as `DYNAMIC`.

The consequences, all silent:

- A bound `min` reaches the component implementation as the raw `{path: '...'}` object: never resolved, never subscribed. The React implementation's defensive check (`typeof props.min === 'string' ? props.min : undefined`) then discards the object, so the constraint is simply dropped.
- The type layer and the runtime disagree about setters. `GenerateSetters` sees the `DataBinding` branch in the union and declares `setMin`/`setMax` on the resolved props type, but the binder only generates runtime setters for `DYNAMIC` properties, so the typed method is `undefined` at runtime.

The trap is not specific to `DateTimeInput`: any catalog schema that wraps a `Dynamic*` schema inside a wider union (hand-written Zod catalogs can) gets the same misclassification.

## Steps to Reproduce

1. Scrape the shipped `DateTimeInput` schema:

```ts
import {scrapeSchemaBehavior} from '@a2ui/web_core/v0_9';
import {DateTimeInputApi} from '@a2ui/web_core/v0_9/basic_catalog';

const behavior = scrapeSchemaBehavior(DateTimeInputApi.schema);
console.log(behavior.shape.min); // {type: 'STATIC'} - wrong
console.log(behavior.shape.value); // {type: 'DYNAMIC'} - correct, same DynamicStringSchema
```

2. Or observe it end to end: render a surface with a `DateTimeInput` whose `min` is `{"path": "/limits/min"}` and any value at `/limits/min` in the data model. The rendered input has no minimum constraint, and updating `/limits/min` has no effect.

## Expected Behavior

`min` and `max` classify as `DYNAMIC` like their sibling `value`: a bound value is resolved, subscribed, and delivered to the component as a string, and the generated setters exist at runtime as the props type promises.

## Environment Details

- **OS**: macOS 15
- **Browser/Platform**: Node.js 22
- **SDK/Package Name & Version**: `@a2ui/web_core` 0.10.7 (reproduced against `main` @ `0dc4a30f`)
- **Protocol Version**: v0.9,v0.9.1
- **Agent Framework & LLM Model**: n/a

## Additional Context

**Suggested fix** - make the structural union check recurse: a union any of whose branches is itself dynamic (nested `Dynamic*` union, or a branch carrying a `Dynamic*`/`DataBinding` `REF:` description) is dynamic. The fix belongs in the scraper rather than in the `DateTimeInput` schema, because third-party catalogs composing `Dynamic*` schemas into wider unions hit the same misclassification.

Prior art on the `v1_0` branch, where this is already solved twice over:

- the v1.0 basic catalog flattened `min`/`max` to a bare `DynamicStringSchema` with the `REF:` marker preserved, removing the trigger
- #2353 rewrites the classifier so the union check reads each option's `REF:` definition name (`getRefDefName(o).startsWith('Dynamic')`), which classifies this exact shape as `DYNAMIC`

Guida per i contributori

Apri la guida per i contributori

Direzione di ricerca

The bug is in `renderers/web_core/src/v0_9/rendering/generic-binder.ts` in the `getFieldBehavior` function. Start by examining how it classifies properties from Zod schemas, focusing on the union check. The `DateTimeInput` component in `renderers/web_core/src/v0_9/basic_catalog/components/basic_components.ts` provides a concrete example with its `min` and `max` properties. Write a test using `scrapeSchemaBehavior` to verify the fix makes nested dynamic unions classify as DYNAMIC. Done looks like the scraper correctly identifying dynamic properties in nested unions and the component receiving resolved string values.

Scritto dal modello di indicizzazione a partire dal testo della issue.

Valutazione

Stack tecnologico
typescript
Ambito
frontend, tooling
Tipo di issue
Bug
Difficoltà
3/5
Tempo stimato
1-2 giorni
Stato di attività
Attiva
Chiarezza
Specificata chiaramente
Idoneità per principianti
65/100

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.