Skip to content

Bug report: Freezed generates List<dynamic> in $CopyWith for List<ConcreteSubtype> when combining union + generic interface/mixin #1339

@noacavalcante

Description

@noacavalcante

Hi! I'm hitting a compilation error caused by generated code typing a List<ConcreteType> field as List<dynamic> inside $CopyWith.

This is happening in a @Freezed(unionKey: ...) sealed union that implements a generic interface Task<T extends BaseItem> and mixes in logic TaskLogic<T>. The field is explicitly declared as List<StepItem> in both union cases.

Expected

Generated $CopyWith should keep the concrete type:

  • List<StepItem> items (not List<dynamic>)
  • no null-assertion items! since the field is required/non-nullable

Actual

Generated code contains:

  • $TaskModelCopyWith.call(... items: List<dynamic> ...)
  • casts like as List<dynamic>
  • uses items! even though the field is non-nullable/required

This leads to compilation error in the generated file.

Reproduction

1) Models / Interfaces / mixins

// base_item.dart
import 'package:freezed_annotation/freezed_annotation.dart';
part 'base_item.freezed.dart';
part 'base_item.g.dart';

@freezed
sealed class BaseItem with _$BaseItem {
  const BaseItem._();

  const factory BaseItem({
    required int order,
  )} = _BaseItem;

  const factory BaseItem.stepItem({
    required int order,
  }) = StepItem;

  factory BaseItem.fromJson(Map<String, dynamic> json) => _$BaseItemFromJson(json);
}
// task_interfaces.dart
abstract class Task<T extends BaseItem> {
  List<T> get items;
}

mixin TaskLogic<T extends BaseItem> {
  List<T> get items;
  List<T> get orderedItems => List.of(items)..sort((a, b) => a.order - b.order);
  int get itemsCount => items.length;
}

2) The working union

// task_a_model.dart
import 'package:freezed_annotation/freezed_annotation.dart';
import 'task_interfaces.dart';
import 'item.dart';

part 'task_a_model.freezed.dart';
part 'task_a_model.g.dart';

@Freezed(unionKey: 'type')
sealed class TaskAModel with _$TaskAModel {
  const TaskAModel._();

  @FreezedUnionValue('a')
  @Implements<Task>()
  @With<TaskLogic>()
  const factory TaskAModel.a({
    required List<BaseItem> items,
  }) = _A;

  @FreezedUnionValue('b')
  const factory TaskAModel.b({
    required String name,
  }) = _B;
}

3) The failing union

// task_b_model.dart
import 'package:freezed_annotation/freezed_annotation.dart';
import 'task_interfaces.dart';
import 'item.dart';

part 'task_b_model.freezed.dart';
part 'task_b_model.g.dart';

@Freezed(unionKey: 'type')
sealed class TaskBModel
    with _$TaskBModel, TaskLogic<StepItem>
    implements Task<StepItem> {
  const TaskBModel._();

  @FreezedUnionValue('a')
  const factory TaskBModel.a({
    required List<StepItem> items,
  }) = _A;

  @FreezedUnionValue('b')
  const factory TaskBModel.b({
    required List<StepItem> items,
    required String extra,
  }) = _B;

  factory TaskBModel.fromJson(Map<String, dynamic> json) => _$TaskBModelFromJson(json);
}

4) Generated output (problematic excerpt)

In the generated file, $TaskBModelCopyWith uses List<dynamic> instead of List<StepItem>, and may emit items!.

Example (illustrative):

abstract mixin class $TaskBModelCopyWith<$Res> {
  $Res call({ List<dynamic> items, ... });
}

Environment

  • Dart: 3.11.0
  • Flutter: 3.41.1
  • freezed: 3.2.5
  • freezed_annotation: 3.1.0
  • build_runner: 2.11.1
  • json_serializable: 6.12.0

Metadata

Metadata

Assignees

Labels

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions