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:
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.
- 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.
- 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.
Currently, GenUI uses a bespoke JSON Schema walker (
_validateInstanceinsideSurfaceDefinition.validate) to validate incoming components.While this custom validator correctly enforces structural keywords like
const,enum,required, andproperties, it doesn't considertype.Becausetypeis not validated, per-property primitive-type mismatches silently pass the validation layer.Example: suppose a
MyButtoncomponent expects alabelof typestringand awidthof typeinteger, the following malformed component will successfully pass validation:{"id": "btn", "component": "Button", "label": 42, "width": "two"}This bad data lands in
ComponentModel.propertieswith the wrong types, silently corrupting the data model and eventually causing a crash at render time when the Flutter widget expects aStringbut finds anint.Also, here's a failing test that captures the issue:
Initially, I was confused as to why we aren't using the
json_schema_builderpackage to validate these components. However, I there are a few reasons:Schema.validate()is async.SurfaceController.handleMessageis a synchronousvoidfunction responding toStream.listen(...). Going async would ripple through callers and could involve a good amount of refactoring.{components, styles, functions}). If we feed a component payload directly to the package, it will fail becausestylesandfunctionsare missing. The hand-rolled implementation inSurfaceDefinition.validatedrills down into theoneOfbranch and validates the component against only its specific sub-schema.A2uiValidationExceptioncontaining a formattedpathon the first failure, which is optimized for relaying over our protocol back to the server.Fixes
Band-aid fix: extend the
_validateInstancemethod to explicitly check thetypekeyword (and ideallyadditionalProperties).Proper fix (maybe): Delegate entirely to
json_schema_builderto close all spec gaps. This will require:SurfaceController.handleMessageasync.ValidationErrorlists intoA2uiValidationExceptions.