diff --git a/packages/genui/lib/src/model/a2ui_message.dart b/packages/genui/lib/src/model/a2ui_message.dart index df0d7f305..ff8622141 100644 --- a/packages/genui/lib/src/model/a2ui_message.dart +++ b/packages/genui/lib/src/model/a2ui_message.dart @@ -121,6 +121,9 @@ sealed class A2uiMessage { ], ); } + + /// Converts this message to a JSON map. + Map toJson(); } /// An A2UI message that signals the client to create and show a new surface. @@ -155,13 +158,15 @@ final class CreateSurface extends A2uiMessage { /// If true, the client sends the full data model in A2A metadata. final bool sendDataModel; - /// Converts this message to a JSON map. + @override Map toJson() => { 'version': 'v0.9', - surfaceIdKey: surfaceId, - 'catalogId': catalogId, - 'theme': ?theme, - 'sendDataModel': sendDataModel, + 'createSurface': { + surfaceIdKey: surfaceId, + 'catalogId': catalogId, + 'theme': theme, + 'sendDataModel': sendDataModel, + }, }; } @@ -187,10 +192,16 @@ final class UpdateComponents extends A2uiMessage { final List components; /// Converts this message to a JSON map. + /// + /// The result is compatible with [A2uiMessage.fromJson] for round-trip + /// serialization (e.g. when saving messages locally). + @override Map toJson() => { 'version': 'v0.9', - surfaceIdKey: surfaceId, - 'components': components.map((c) => c.toJson()).toList(), + 'updateComponents': { + surfaceIdKey: surfaceId, + 'components': components.map((c) => c.toJson()).toList(), + }, }; } @@ -224,12 +235,14 @@ final class UpdateDataModel extends A2uiMessage { /// key at the path. final Object? value; - /// Converts this message to a JSON map. + @override Map toJson() => { 'version': 'v0.9', - surfaceIdKey: surfaceId, - 'path': path.toString(), - 'value': ?value, + 'updateDataModel': { + surfaceIdKey: surfaceId, + 'path': path.toString(), + 'value': value, + }, }; } @@ -246,6 +259,9 @@ final class DeleteSurface extends A2uiMessage { /// The ID of the surface that this message applies to. final String surfaceId; - /// Converts this message to a JSON map. - Map toJson() => {'version': 'v0.9', surfaceIdKey: surfaceId}; + @override + Map toJson() => { + 'version': 'v0.9', + 'deleteSurface': {surfaceIdKey: surfaceId}, + }; } diff --git a/packages/genui/lib/test/validation.dart b/packages/genui/lib/test/validation.dart index 1b405b539..99396a7b4 100644 --- a/packages/genui/lib/test/validation.dart +++ b/packages/genui/lib/test/validation.dart @@ -81,8 +81,10 @@ Future> validateCatalogItemExamples( components: components, ); + final payload = + surfaceUpdate.toJson()['updateComponents'] as Map; final List validationErrors = await schema.validate( - surfaceUpdate.toJson(), + payload, ); if (validationErrors.isNotEmpty) { errors.add( diff --git a/packages/genui/test/model/a2ui_message_test.dart b/packages/genui/test/model/a2ui_message_test.dart index bab54c0a4..82a651097 100644 --- a/packages/genui/test/model/a2ui_message_test.dart +++ b/packages/genui/test/model/a2ui_message_test.dart @@ -105,6 +105,66 @@ void main() { expect(message.toJson(), containsPair('version', 'v0.9')); }); + test('CreateSurface round-trip: toJson then fromJson', () { + const message = CreateSurface( + surfaceId: 's1', + catalogId: 'c1', + theme: {'color': 'blue'}, + sendDataModel: true, + ); + final decoded = A2uiMessage.fromJson( + Map.from(message.toJson()), + ); + expect(decoded, isA()); + final create = decoded as CreateSurface; + expect(create.surfaceId, message.surfaceId); + expect(create.catalogId, message.catalogId); + expect(create.theme, message.theme); + expect(create.sendDataModel, message.sendDataModel); + }); + + test('UpdateComponents round-trip: toJson then fromJson', () { + const message = UpdateComponents( + surfaceId: 's1', + components: [ + Component(id: 'c1', type: 'Text', properties: {'text': 'Hi'}), + ], + ); + final decoded = A2uiMessage.fromJson( + Map.from(message.toJson()), + ); + expect(decoded, isA()); + final update = decoded as UpdateComponents; + expect(update.surfaceId, message.surfaceId); + expect(update.components.length, message.components.length); + expect(update.components.first.id, message.components.first.id); + }); + + test('UpdateDataModel round-trip: toJson then fromJson', () { + final message = UpdateDataModel( + surfaceId: 's1', + path: DataPath('/user/name'), + value: 'Alice', + ); + final decoded = A2uiMessage.fromJson( + Map.from(message.toJson()), + ); + expect(decoded, isA()); + final update = decoded as UpdateDataModel; + expect(update.surfaceId, message.surfaceId); + expect(update.path, message.path); + expect(update.value, message.value); + }); + + test('DeleteSurface round-trip: toJson then fromJson', () { + const message = DeleteSurface(surfaceId: 's1'); + final decoded = A2uiMessage.fromJson( + Map.from(message.toJson()), + ); + expect(decoded, isA()); + expect((decoded as DeleteSurface).surfaceId, message.surfaceId); + }); + test('fromJson throws on unknown message type', () { final json = {'version': 'v0.9', 'unknown': {}}; expect( diff --git a/packages/genui/test/test_infra/validation_test_utils.dart b/packages/genui/test/test_infra/validation_test_utils.dart index 415f75d48..62ce45bf9 100644 --- a/packages/genui/test/test_infra/validation_test_utils.dart +++ b/packages/genui/test/test_infra/validation_test_utils.dart @@ -53,8 +53,11 @@ void validateCatalogExamples( components: components, ); + final payload = + surfaceUpdate.toJson()['updateComponents'] + as Map; final List validationErrors = await schema.validate( - surfaceUpdate.toJson(), + payload, ); expect(validationErrors, isEmpty); });