Skip to content

Component schema validation silently ignores primitive type mismatches #954

@andrewkolos

Description

@andrewkolos

Currently, GenUI uses a bespoke JSON Schema walker (_validateInstance inside SurfaceDefinition.validate) to validate incoming components.

While this custom validator correctly enforces structural keywords like const, enum, required, and properties, it doesn't consider type.Because type is not validated, per-property primitive-type mismatches silently pass the validation layer.

Example: suppose a MyButton component expects a label of type string and a width of type integer, the following malformed component will successfully pass validation:

{"id": "btn", "component": "Button", "label": 42, "width": "two"}

This bad data lands in ComponentModel.properties with the wrong types, silently corrupting the data model and eventually causing a crash at render time when the Flutter widget expects a String but finds an int.

Also, here's a failing test that captures the issue:

// in ui_models_test.dart
 test('validate enforces primitive types (this will fail due to the bug)', () {
    final component = const Component(
      id: 'test',
      type: 'Text',
      // oopsies, text is a number
      properties: {'text': 42},
    );
    final surfaceDefinition = SurfaceDefinition(
      surfaceId: 's1',
      components: {'test': component},
    );

    final schema = S.object(
      properties: {
        'components': S.list(
          items: S.object(
            properties: {
              'component': S.string(constValue: 'Text'),
              'text': S.string(), // Validator should enforce this.
            },
          ),
        ),
      },
    );

    expect(
      () => surfaceDefinition.validate(schema),
      throwsA(isA<A2uiValidationException>()),
    );
  });
});

Initially, I was confused as to why we aren't using the json_schema_builder package to validate these components. However, I there are a few reasons:

  1. Schema.validate() is async. SurfaceController.handleMessage is a synchronous void function responding to Stream.listen(...). Going async would ripple through callers and could involve a good amount of refactoring.
  2. The global catalog schema defines many things ({components, styles, functions}). If we feed a component payload directly to the package, it will fail because styles and functions are missing. The hand-rolled implementation in SurfaceDefinition.validate drills down into the oneOf branch and validates the component against only its specific sub-schema.
  3. The current implementation throws an A2uiValidationException containing a formatted path on the first failure, which is optimized for relaying over our protocol back to the server.

Fixes

Band-aid fix: extend the _validateInstance method to explicitly check the type keyword (and ideally additionalProperties).

Proper fix (maybe): Delegate entirely to json_schema_builder to close all spec gaps. This will require:

  • Make SurfaceController.handleMessage async.
  • Writing an adapter to translate ValidationError lists into A2uiValidationExceptions.

Metadata

Metadata

Assignees

Labels

front-line-handledCan wait until the second-line triage. The front-line triage already checked if it's a P0.

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions