Freezed: Decorator issue with List<Object>

Created on 21 Apr 2020  路  23Comments  路  Source: rrousselGit/freezed

Hi,

Consider the following :

import 'package:flutter/material.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:flutter/foundation.dart';

import './converter/widgetList_converter.dart';

part 'testFreezed.freezed.dart';
part 'testFreezed.g.dart';

@freezed
abstract class Test2 with _$Test2 {
  const factory Test2({
    @ListWidgetModelConv() List<WidgetModel> widgets,
  }) = _Test2;

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

Generate :

// GENERATED CODE - DO NOT MODIFY BY HAND

part of 'testFreezed.dart';

// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************

_$_Test2 _$_$_Test2FromJson(Map<String, dynamic> json) {
  return _$_Test2(
    widgets: json['widgets'] as List,
  );
}

Map<String, dynamic> _$_$_Test2ToJson(_$_Test2 instance) {
  final val = <String, dynamic>{};

  void writeNotNull(String key, dynamic value) {
    if (value != null) {
      val[key] = value;
    }
  }

  writeNotNull('widgets', instance.widgets);
  return val;
}

Where with the corresponding Json_serialisable :

import 'package:flutter/material.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:flutter/foundation.dart';

import './converter/widgetList_converter.dart';

part 'test.g.dart';

@JsonSerializable(nullable: false)
class Test {
  Test({
    this.widgets,
  });

  factory Test.fromJson(Map<String, dynamic> json) => _$TestFromJson(json);
  Map<String, dynamic> toJson() => _$TestToJson(this);

  @ListWidgetModelConv()
  final List<WidgetModel> widgets;
}

Generate :

// GENERATED CODE - DO NOT MODIFY BY HAND

part of 'test.dart';

// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************

Test _$TestFromJson(Map<String, dynamic> json) {
  return Test(
    widgets: const ListWidgetModelConv()
        .fromJson(json['widgets'] as Map<String, dynamic>),
  );
}

Map<String, dynamic> _$TestToJson(Test instance) {
  final val = <String, dynamic>{};

  void writeNotNull(String key, dynamic value) {
    if (value != null) {
      val[key] = value;
    }
  }

  writeNotNull('widgets', const ListWidgetModelConv().toJson(instance.widgets));
  return val;
}

Could you have a look ?

bug

All 23 comments

By the way, replacing

@ListWidgetModelConv() List<WidgetModel> widgets,

by

@JsonKey(fromJson: fromJsons, toJson: toJsons) List<WidgetModel> widgets,

bypass it and produce expected result, but the decorator way should work.

Do we share the same opinion on this ?
Don't hesitate to let me know, if you need any thing else.

What's ListWidgetModelConv?

It's a converter to handle serialisation/deserialisation :

import 'package:json_annotation/json_annotation.dart';

class ListWidgetModelConv
    implements JsonConverter<List<WidgetModel>, Map<String, dynamic>> {
  const ListWidgetModelConv();

  List<WidgetModel> fromJson(Map<String, dynamic> json) {
    if (json == null) {
      return <WidgetModel>[];
    }
    List<WidgetModel> result = <WidgetModel>[];
    for (String key in json.keys) {
      result.add(WidgetModel.fromJson(json[key]));
    }
    return result;
  }

  Map<String, dynamic> toJson(List<WidgetModel> object) {
    if (object == null) {
      return null;
    }
    Map<String, dynamic> result = <String, dynamic>{};
    object.asMap().forEach(
        (int key, WidgetModel val) => result[key.toString()] = val.toJson());

    return result;
  }
}

With type not in List, json_serialisable and Freezed get along just fine.

This converter for exemple, is handle properly:

import 'package:json_annotation/json_annotation.dart';

class DateTimeIntConv implements JsonConverter<DateTime, int> {
  const DateTimeIntConv();

  @override
  DateTime fromJson(int json) =>
      json == null ? null : DateTime.fromMillisecondsSinceEpoch(json);

  @override
  int toJson(DateTime object) =>
      object == null ? null : object.millisecondsSinceEpoch;
}

But looking at the freezed part, it seems, the type of the object from the list is smith to dynamic. By doing so, tthe converter don't match anymore. That's my 2 cents analysis.

I just try with

class ListWidgetModelConv
    implements JsonConverter<List<dynamic>, Map<String, dynamic>> {

it generate fine with :

// GENERATED CODE - DO NOT MODIFY BY HAND

part of 'testFreezed.dart';

// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************

_$_Test2 _$_$_Test2FromJson(Map<String, dynamic> json) {
  return _$_Test2(
    widgets: const ListWidgetModelConv()
        .fromJson(json['widgets'] as Map<String, dynamic>),
  );
}

Map<String, dynamic> _$_$_Test2ToJson(_$_Test2 instance) {
  final val = <String, dynamic>{};

  void writeNotNull(String key, dynamic value) {
    if (value != null) {
      val[key] = value;
    }
  }

  writeNotNull('widgets', const ListWidgetModelConv().toJson(instance.widgets));
  return val;
}

but my list contain only one type of object, so I presume I should be able to not to downcast it dynamic.

I'm I missing something ?

What's WidgetModel?

It's an object of my project. Don't matter on the issue.

I you prefer, I could replace if with another common object, like Datetime, Color..

Ok, with DateTime it generate : List<DateTime> get datetime;

So I understand why you wanted to know what was WidgetModel. It's another class generated with Freezed.

@freezed
abstract class Container with _$Container {
  const factory Container.playlist({
    @required
        String name,
    )
       @ListWidgetModelConv()
        List<WidgetModel> currentWidgets,
  }) = PlayListModel;

  const factory EasivioContainer.widget({
    @required
        String name,
        @ListWidgetModelConv()
        List<WidgetModel> widgets,
  }) = WidgetModel;

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

It sounds like json_serializable is unable to access WidgetsModel for some reason

If you look in the file generated by, is "widgets" a List internally too?

inside container_model.freezed.dart found this :

@JsonSerializable()
class _$WidgetModel with DiagnosticableTreeMixin implements WidgetModel {
  const _$WidgetModel(
      {
      @required
          this.name,
      @ListWidgetModelConv()
          this.widgets,
      : assert(name != null),

  final List<dynamic> widgets;

So still a List but of dynamic instead of WidgetModel.

I wonder, if this is because, the object contain a list on himself, that the generation on the freezed part loose the type. (nope that work, related to Union then?)

Okay, sorry for disturbing you with Json_serialisable...

import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:freezed_annotation/freezed_annotation.dart';

part 'test_model.freezed.dart';
part 'test_model.g.dart';

@freezed
abstract class TestModel with _$TestModel {
  const factory TestModel.model({
    List<Model> testModel,
  }) = Model;

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

This simplified exemple is better.
List<Model> is converted to List<dynamic>

// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named

part of 'test_model.dart';

// **************************************************************************
// FreezedGenerator
// **************************************************************************

T _$identity<T>(T value) => value;
TestModel _$TestModelFromJson(Map<String, dynamic> json) {
  return Model.fromJson(json);
}

class _$TestModelTearOff {
  const _$TestModelTearOff();

  Model model({List<dynamic> testModel}) {
    return Model(
      testModel: testModel,
    );
  }
}

// ignore: unused_element
const $TestModel = _$TestModelTearOff();

mixin _$TestModel {
  List<dynamic> get testModel;

  Map<String, dynamic> toJson();
  $TestModelCopyWith<TestModel> get copyWith;
}

abstract class $TestModelCopyWith<$Res> {
  factory $TestModelCopyWith(TestModel value, $Res Function(TestModel) then) =
      _$TestModelCopyWithImpl<$Res>;
  $Res call({List<dynamic> testModel});
}

class _$TestModelCopyWithImpl<$Res> implements $TestModelCopyWith<$Res> {
  _$TestModelCopyWithImpl(this._value, this._then);

  final TestModel _value;
  // ignore: unused_field
  final $Res Function(TestModel) _then;

  @override
  $Res call({
    Object testModel = freezed,
  }) {
    return _then(_value.copyWith(
      testModel:
          testModel == freezed ? _value.testModel : testModel as List<dynamic>,
    ));
  }
}

abstract class $ModelCopyWith<$Res> implements $TestModelCopyWith<$Res> {
  factory $ModelCopyWith(Model value, $Res Function(Model) then) =
      _$ModelCopyWithImpl<$Res>;
  @override
  $Res call({List<dynamic> testModel});
}

class _$ModelCopyWithImpl<$Res> extends _$TestModelCopyWithImpl<$Res>
    implements $ModelCopyWith<$Res> {
  _$ModelCopyWithImpl(Model _value, $Res Function(Model) _then)
      : super(_value, (v) => _then(v as Model));

  @override
  Model get _value => super._value as Model;

  @override
  $Res call({
    Object testModel = freezed,
  }) {
    return _then(Model(
      testModel:
          testModel == freezed ? _value.testModel : testModel as List<dynamic>,
    ));
  }
}

@JsonSerializable()
class _$Model with DiagnosticableTreeMixin implements Model {
  const _$Model({this.testModel});

  factory _$Model.fromJson(Map<String, dynamic> json) =>
      _$_$ModelFromJson(json);

  @override
  final List<dynamic> testModel;

  @override
  String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) {
    return 'TestModel.model(testModel: $testModel)';
  }

  @override
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
    properties
      ..add(DiagnosticsProperty('type', 'TestModel.model'))
      ..add(DiagnosticsProperty('testModel', testModel));
  }

  @override
  bool operator ==(dynamic other) {
    return identical(this, other) ||
        (other is Model &&
            (identical(other.testModel, testModel) ||
                const DeepCollectionEquality()
                    .equals(other.testModel, testModel)));
  }

  @override
  int get hashCode =>
      runtimeType.hashCode ^ const DeepCollectionEquality().hash(testModel);

  @override
  $ModelCopyWith<Model> get copyWith =>
      _$ModelCopyWithImpl<Model>(this, _$identity);

  @override
  Map<String, dynamic> toJson() {
    return _$_$ModelToJson(this);
  }
}

abstract class Model implements TestModel {
  const factory Model({List<dynamic> testModel}) = _$Model;

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

  @override
  List<dynamic> get testModel;
  @override
  $ModelCopyWith<Model> get copyWith;
}

Oh, so this is a recursive class

Instead of:

    List<Model> testModel,

do

    List<TestModel> testModel,

Still not quite, might have oversimplified the exemple :

import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:freezed_annotation/freezed_annotation.dart';

part 'test_model.freezed.dart';
part 'test_model.g.dart';

@freezed
abstract class A with _$A {
  const factory A.b({
    String name,
    List<B> objectB,
  }) = B;

  const factory A.c({
    String name,
    List<C> objectC,
  }) = C;
}

I need to ensure C list doesn't contains B objects, but need the shared property to access the name.
Does my explanation make sense ?

Why are both B and C under A?
Would it be logical to separate them?

@freezed
abstract class B with _$B {
  const factory B({
    String name,
    List<B> objectB,
  }} = _B;
}

@freezed
abstract class C with _$C {
  const factory C({
    String name,
    List<C> objectC,
  }} = _C;
}

B and C shared some properties. That's why I wanted to use the Union.
How would you write A then ?

import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:freezed_annotation/freezed_annotation.dart';

part 'test_model.freezed.dart';
part 'test_model.g.dart';

@freezed
abstract class A with _$A {
  const factory A.b({
    String name,
    List<B> objectB,
  }) = _B;

  const factory A.c({
    String name,
    List<C> objectC,
  }) = _C;
}
@freezed
abstract class B with _$B {
  const factory B({
    String name,
    List<B> objectB,
  }} = _B;
}

@freezed
abstract class C with _$C {
  const factory C({
    String name,
    List<C> objectC,
  }} = _C;
}

Indeed, work well ! But is there a way not to repeat the entire class B and C in A ? (reason why I put them both in A and remove the private at the beginning to make them public)

You could have:

@freezed
abstract class A with _$A {
  const factory A.b(B b) = _Ab;
  const factory A.c(C c) = _Ac;
}

@freezed
abstract class B with _$B {
  const factory B({
    String name,
    List<B> objectB,
  }} = _B;
}

@freezed
abstract class C with _$C {
  const factory C({
    String name,
    List<C> objectC,
  }} = _C;
}

But seems that I loose the shared properties on name field then.

I can't write A.name anymore.

You can add it manually:

@freezed
abstract class A implements _$A {
  A._();
  factory A.b(B b) = _Ab;
  factory A.c(C c) = _Ac;

  String get name {
    return map(
      b: (b) => b.name,
      c: (c) => c.name,
    );
  }
}

Remy, thank you for taking the time.

But it's like redeclaring the model a second time. At least by copy pasting it, not DRY but easier to update.
Maybe that could be an evolution one day.

Until then feels like https://github.com/rrousselGit/freezed/issues/150#issuecomment-617080527 should work cause it takes advantage of the shared properties.
I replaced it manually in the generated file, and it compile, so wonder why it generate with dynamic instead of the real classe type.

Regards.

Because during the step of analyzing the code you wrote to generate Model, then Model doesn't exist yet.

So List<Model> is interpreted as List<dynamic> by Dart's analysis server, and Freezed generated code in consequence

Run into https://github.com/rrousselGit/freezed/issues/142 with https://github.com/rrousselGit/freezed/issues/150#issuecomment-617106811

Will keep an eyes out for it, and give it another try later.
Thanks

@bounty1342 once #204 lands it should help at least with the List having List in the freezed generated code.

Since #204 is merged, is this still a problem?

Was this page helpful?
0 / 5 - 0 ratings

Related issues

rahulnakre picture rahulnakre  路  3Comments

tbm98 picture tbm98  路  3Comments

pblinux picture pblinux  路  4Comments

ndinhbang picture ndinhbang  路  5Comments

alfinbi picture alfinbi  路  4Comments