Skip to content

[BUG]: genui's validation checks never fail #2853

Closed
flutter/genui
#1045

Description

@diegolopezrm
  • I have searched the existing issues to make sure this bug has not already been reported.

Describe the Bug

A CheckRule the agent sends never fails in genui. An agent can say a field is required, the user can leave it empty, and the component reports itself valid. CheckBox, Slider and ChoicePicker all go through the same helper, so it is all of them.

There are two separate reasons, and either one on its own is enough to make validation inert.

The functionCall wrapper is not understood. checksToExpression (widget_helpers.dart:145) wraps the combined condition:

return {
  'functionCall': {
    'call': 'and',
    'args': {'values': checks.map((c) => c['condition']).toList()},
  },
};

evaluateConditionStream does not read that shape, and what it cannot read it treats as true. Measured against main, with /email holding an empty string:

expression result
{call: required, args: {value: {path: /email}}} false
{call: and, args: {values: [false]}} false
{functionCall: {call: and, args: {values: [false]}}} true
{functionCall: {call: noSuchFunction, args: {}}} true

The last row is the one that worries me: a function that does not exist passes, so nothing fails loudly enough to be noticed.

and sees its arguments unresolved. Dropping the wrapper is not enough. _isTruthy (basic_functions.dart:52) is:

bool _isTruthy(Object? value) {
  if (value is bool) return value;
  if (value == null) return false;
  return true;
}

and AndFunction.executeSync reads args['values'] straight out of the map. A {path: ...} or a nested {call: ...} arrives as a Map, which is not a bool and not null, so it counts as true:

expression result
{call: and, args: {values: [false]}} false
{call: and, args: {values: [{path: /no}]}} where /no is false true
{call: and, args: {values: [{call: required, args: {value: {path: /email}}}]}} true

or and not read the same helper.

Steps to Reproduce

final model = InMemoryDataModel();
model.update(DataPath('/email'), '');
final context = DataContext(
  model,
  DataPath.root,
  functions: BasicCatalogItems.asCatalog().functions,
);

final checks = <JsonMap>[
  {
    'condition': {
      'call': 'required',
      'args': {'value': {'path': '/email'}},
    },
    'message': 'Required.',
  },
];

// false, as it should be.
print(await context.evaluateConditionStream(checks.first['condition']).first);

// true, with the same rule, through the path the catalog actually uses.
print(await context.evaluateConditionStream(checksToExpression(checks)).first);

Expected Behavior

A rule whose condition resolves to false makes the component invalid, and an expression the engine cannot resolve fails loudly rather than passing.

Additional Context

One more thing that falls out of this, and it is a design question rather than a bug: checksToExpression keeps only the conditions and drops every message. CheckRule requires one ("The error message to display if the check fails") and it is the only part of the rule a person ever reads. Combining the rules into a single and means the client knows it is invalid but not what to say about it, so the basic catalog can only colour a field red. Evaluating the rules one at a time costs nothing more and keeps the sentence the agent wrote.

Happy to send a PR for either half if the shape above looks right to you.

Activity

  1. added
    status: needs-triageauto-managed: https://github.com/a2ui-project/a2ui/blob/main/scripts/triage.mjs
    on Sep 28, 2026
  2. nan-yu commented on Sep 28, 2026

    @nan-yu
    Collaborator

    A2UI Triage: Thanks for reporting. We will investigate the CheckRule validation dispatching in genui. cc @polina-c

  3. removed
    status: needs-triageauto-managed: https://github.com/a2ui-project/a2ui/blob/main/scripts/triage.mjs
    on Sep 28, 2026
  4. jacobsimionato commented on Sep 29, 2026

    @jacobsimionato
    Collaborator

    We investigated this issue across the A2UI specification and reference implementations in other languages (TypeScript web_core, Swift A2UICore, and Python a2ui_core), and opened a fix in flutter/genui#1045.

    Here is a summary of the analysis and why the fix was implemented this way:

    1. Protocol Specification Analysis (DynamicBoolean vs Action)

    • In common_types.json, a CheckRule's condition is typed as a DynamicBoolean, which is a union of a literal boolean, DataBinding ({"path": ...}), or FunctionCall ({"call": "...", "args": ...}).
    • The 'functionCall' envelope only exists under Action to distinguish between {"event": ...} and {"functionCall": ...}. It does not belong on dynamic expressions or boolean conditions.
    • Wrapping conditions in {'functionCall': {'call': 'and', ...}} in checksToExpression was invalid protocol syntax, which caused DataContext._evaluateStream (which looks for path and call) to treat the expression as an opaque literal object.

    2. Multi-Language Comparison on List Resolution & Logical Functions

    • In catalog.json, the and and or functions specify args.values as an array of DynamicBooleans (minItems: 2).
    • In TypeScript (renderers/web_core/src/v0_9/rendering/data-context.ts), resolveSignal and resolveDynamicValue explicitly check Array.isArray(value), recursively resolving each array item with this.resolveSignal(item) / this.resolveDynamicValue(item) before combining them.
    • In Swift (NodeResolver.swift) and Python (generic_binder.py / function_impls.py), arguments and dynamic values inside collections are similarly evaluated recursively so that and and or receive evaluated booleans.
    • In genui, _evaluateStream only handled Map objects and fell back to treating Lists as literals. Consequently, AndFunction.executeSync received raw Maps ({'path': ...} or {'call': ...}) which _isTruthy incorrectly treated as truthy because they were non-null and not booleans.

    3. Preserving CheckRule.message (The Design Question)

    • The spec states that CheckRule.message is required in v0.9 ("The error message to display if the check fails").
    • In TypeScript web_core (generic-binder.ts), Swift (ResolvedCheck.swift), and Python (generic_binder.py), each check rule is evaluated individually and active failures are collected into validationErrors: string[].
    • In genui, TextField, DateTimeInput, and Button were already using ValidationHelper.validateStream, which evaluates rules independently and retains the authored message. Only CheckBox, Slider, and ChoicePicker had used checksToExpression with hardcoded fallback strings ('Invalid value', 'Invalid selection').
    • Migrating CheckBox, Slider, and ChoicePicker to ValidationHelper.validateStream aligns them with the rest of genui and with the other A2UI renderers, ensuring agent-authored messages are displayed to users.

    4. Truthiness and Handling Failed / Unknown Expressions

    • When an expression cannot be resolved (e.g. unknown function or invalid map), evaluateConditionStream in genui previously did return v != null;, which caused any unhandled Map to silently pass validation as true.
    • Unified truthiness semantics into a shared isTruthy(Object? value) helper that properly evaluates primitives (bool, non-empty String, non-zero num, non-empty List) and structured ValidationResult objects (v['valid'] == true), while ensuring unknown functions or unresolvable expression maps evaluate to false.

    The pull request with the fix, unit tests, and an end-to-end integration test reproducing this issue is available at: flutter/genui#1045

  5. added
    P1This is a priority. Team is actively working on it, or will be soon.
    and removed
    P2The team intends to work on this in the near future, or at a lower priority.
    on Sep 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

Type

No type

Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions