Skip to content

[BUG] [genui] CatalogItem has no copyWith, and reconstructing one from dataSchema duplicates "component" in required properties #2829

Description

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

Describe the Bug

CatalogItem (packages/genui/lib/src/model/catalog_item.dart) is declared as final class CatalogItem with a const constructor that takes required Schema dataSchema:

const CatalogItem({
  required this.name,
  required Schema dataSchema,
  required this.widgetBuilder,
  this.exampleData = const [],
  this.isImplicitlyFlexible = false,
}) : _originalSchema = dataSchema;

_originalSchema is private. When item.dataSchema is called, it constructs an ObjectSchema on the fly, injecting the 'component' property and prepending 'component' into required:

ObjectSchema get dataSchema {
  ...
  final List<Object?> requiredProps =
      originalMap['required'] as List<Object?>? ?? <Object?>[];

  return ObjectSchema.fromMap(<String, Object?>{
    ...originalMap,
    'properties': <String, Object?>{
      ...properties,
      'component': <String, Object?>{
        'type': 'string',
        'enum': <String>[name],
      },
    },
    'required': <Object?>['component', ...requiredProps],
    'additionalProperties': true,
  });
}

Because CatalogItem does not provide a copyWith method (unlike Catalog.copyWith), a host application or custom wrapper that wants to customize a widget builder (e.g. to wrap components with analytics, custom styling, or theme decorators) must construct a new CatalogItem:

CatalogItem(
  name: item.name,
  dataSchema: item.dataSchema, // Passes already-injected schema
  widgetBuilder: (ctx) => wrapWithTheme(item.widgetBuilder(ctx)),
)

Because item.dataSchema already contains 'component' in requiredProps, the new CatalogItem.dataSchema prepends 'component' a second time:
required: ['component', 'component', ...]

If wrapped again (e.g., theming + instrumentation), it accumulates duplicates repeatedly.
JSON Schema specifies that required should be an array of unique strings. When catalog.definition or toCapabilitiesJson() is sent to the LLM agent, this malformed schema is exposed. Furthermore, hand-rolling constructor invocations is fragile because any future fields added to CatalogItem will be dropped.

Steps to Reproduce

import 'package:flutter_test/flutter_test.dart';
import 'package:genui/genui.dart';

void main() {
  test('CatalogItem reconstruction duplicates component in required', () {
    final original = BasicCatalogItems.button;
    final wrapped = CatalogItem(
      name: original.name,
      dataSchema: original.dataSchema,
      widgetBuilder: original.widgetBuilder,
    );

    final requiredProps = wrapped.dataSchema.value['required'] as List<Object?>;
    final componentOccurrences =
        requiredProps.where((prop) => prop == 'component').length;

    expect(componentOccurrences, 1); // Fails: Actual is 2
  });
}

Expected Behavior

  1. CatalogItem should provide a copyWith method:
CatalogItem copyWith({
  String? name,
  Schema? dataSchema,
  CatalogWidgetBuilder? widgetBuilder,
  List<ExampleBuilderCallback>? exampleData,
  bool? isImplicitlyFlexible,
}) => CatalogItem(
  name: name ?? this.name,
  dataSchema: dataSchema ?? _originalSchema,
  widgetBuilder: widgetBuilder ?? this.widgetBuilder,
  exampleData: exampleData ?? this.exampleData,
  isImplicitlyFlexible: isImplicitlyFlexible ?? this.isImplicitlyFlexible,
);
  1. The dataSchema getter should ensure 'component' is not duplicated if already present in requiredProps (e.g., using LinkedHashSet or checking requiredProps.contains('component')).

Environment Details

  • SDK/Package Name & Version: package:genui (packages/genui/lib/src/model/catalog_item.dart)
  • Protocol Version: v0.9.1

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    • Status
      Todo

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions