Complex models with json_serializable - List<objects> not converting to map
Asked Answered
T

1

6

UPDATE I have built a small sample and will add all code to this post. I have to believe there is an answer/explanation to this and am hoping someone can educate me on what I'm missing. Class fields that are type object are not getting converted and I do not understand why.

Here are the model classes I'm working with.

import 'package:json_annotation/json_annotation.dart';

part 'parent.g.dart';

@JsonSerializable()
class Parent {
  int id;
  final String name;
  final int age;
  List<Child> children;
  Job job;

  Parent({this.name, this.age, this.children, this.job});

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

  Map<String, dynamic> toJson() => _$ParentToJson(this);
}

@JsonSerializable()
class Child{
  int id;
  final String name;
  final int age;

  Child({this.name, this.age});

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

  Map<String, dynamic> toJson() => _$ChildToJson(this);
}

@JsonSerializable()
class Job{
  int id;
  String title;

  Job({this.title});

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

  Map<String, dynamic> toJson() => _$JobToJson(this);
}

Here is the .g generated file for these classes

// GENERATED CODE - DO NOT MODIFY BY HAND

part of 'parent.dart';

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

Parent _$ParentFromJson(Map<String, dynamic> json) {
  return Parent(
    name: json['name'] as String,
    age: json['age'] as int,
    children: (json['children'] as List)
        ?.map(
            (e) => e == null ? null : Child.fromJson(e as Map<String, dynamic>))
        ?.toList(),
    job: json['job'] == null
        ? null
        : Job.fromJson(json['job'] as Map<String, dynamic>),
  )..id = json['id'] as int;
}

Map<String, dynamic> _$ParentToJson(Parent instance) => <String, dynamic>{
      'id': instance.id,
      'name': instance.name,
      'age': instance.age,
      'children': instance.children,
      'job': instance.job,
    };

Child _$ChildFromJson(Map<String, dynamic> json) {
  return Child(
    name: json['name'] as String,
    age: json['age'] as int,
  )..id = json['id'] as int;
}

Map<String, dynamic> _$ChildToJson(Child instance) => <String, dynamic>{
      'id': instance.id,
      'name': instance.name,
      'age': instance.age,
    };

Job _$JobFromJson(Map<String, dynamic> json) {
  return Job(
    title: json['title'] as String,
  )..id = json['id'] as int;
}

Map<String, dynamic> _$JobToJson(Job instance) => <String, dynamic>{
      'id': instance.id,
      'title': instance.title,
    };

Here is the DAO class for the parent class

import 'package:sembast/sembast.dart';

import 'package:json_serial_test/services/app_database.dart';
import 'package:json_serial_test/models/parent.dart';

class ParentDao {
  static const String PARENT_STORE_NAME = 'parents';
  // A Store with int keys and Map<String, dynamic> values.
  // This Store acts like a persistent map, values of which are Parent objects converted to Map
  final _parentStore = intMapStoreFactory.store(PARENT_STORE_NAME);

  // Private getter to shorten the amount of code needed to get the
  // singleton instance of an opened database.
  Future<Database> get _db async => await AppDatabase.instance.database;

  Future insert(Parent parent) async {
    await _parentStore.add(await _db, parent.toJson());
  }

  Future update(Parent parent) async {
    // For filtering by key (ID), RegEx, greater than, and many other criteria,
    // we use a Finder.
    final finder = Finder(filter: Filter.byKey(parent.id));
    await _parentStore.update(
      await _db,
      parent.toJson(),
      finder: finder,
    );
  }

  Future deleteAll() async {
    await _parentStore.delete(await _db);
  }

  Future delete(Parent parent) async {
    final finder = Finder(filter: Filter.byKey(parent.id));
    await _parentStore.delete(
      await _db,
      finder: finder,
    );
  }

  Future<List<Parent>> getAllSortedByName() async {
    // Finder object can also sort data.
    final finder = Finder(sortOrders: [
      SortOrder('name'),
    ]);

    final recordSnapshots = await _parentStore.find(
      await _db,
      finder: finder,
    );

    // Making a List<Parent> out of List<RecordSnapshot>
    return recordSnapshots.map((snapshot) {
      final parent = Parent.fromJson(snapshot.value);
      // An ID is a key of a record from the database.
      parent.id = snapshot.key;
      return parent;
    }).toList();
  }
}

Here is my test

// Setup
          final k1 = Child(name: 'Billy', age: 10);
          final k2 = Child(name: 'Jannet', age: 9);
          final job = Job(title: 'Cook');
          final List<Child> kids = [k1, k2];

          final dad = Parent(name: 'Dave', age: 52, job: job, children: kids);

          await pDao.insert(dad);

          List<Parent> dadsInDb = await pDao.getAllSortedByName();

          print('Dads from DB: ${dadsInDb.toString()}');

Upon trying to perform an insert of a Parent into my sembast DB, this is the error that shows up.

E/flutter (12986): [ERROR:flutter/lib/ui/ui_dart_state.cc(157)] Unhandled Exception: Invalid argument(s): value Instance of 'Child' unsupported type Child E/flutter (12986): #0      cloneValue (package:sembast/src/utils.dart:191:3) E/flutter (12986): #1      cloneValue.<anonymous closure> (package:sembast/src/utils.dart:177:33) E/flutter (12986): #2      MappedListIterable.elementAt (dart:_internal/iterable.dart:417:29) E/flutter (12986): #3      ListIterable.toList (dart:_internal/iterable.dart:221:19) E/flutter (12986): #4      cloneValue (package:sembast/src/utils.dart:177:52) E/flutter (12986): #5      cloneValue.<anonymous closure> (package:sembast/src/utils.dart:174:49) E/flutter (12986): #6      MapMixin.map (dart:collection/maps.dart:165:28) E/flutter (12986): #7  cloneValue (package:sembast/src/utils.dart:173:18) E/flutter (12986):
#8      SembastStore.txnPutSync (package:sembast/src/store_impl.dart:133:15) E/flutter (12986): #9     SembastStore.txnAdd (package:sembast/src/store_impl.dart:117:11) E/flutter (12986): <asynchronous suspension> E/flutter (12986): #10    StoreRefMixin.add.<anonymous closure> (package:sembast/src/store_ref_impl.dart:75:12) E/flutter (12986): #11 SembastDatabase.inTransaction.<anonymous closure> (package:sembast/src/database_impl.dart:1238:34) E/flutter (12986):
#12     SembastDatabase.transaction.<anonymous closure>.<anonymous closure> (package:sembast/src/database_impl.dart:1090:59) E/flutter (12986): #13     new Future.sync (dart:async/future.dart:224:31) E/flutter (12986): #14     SembastDatabase.transaction.<anonymous closure> (package:sembast/src/database_impl.dart:1090:26) E/flutter (12986): #15     BasicLock.synchronized (package:synchronized/src/basic_lock.dart:32:26) E/flutter (12986):
#16     SembastDatabase.transaction (package:sembast/src/database_impl.dart:1073:38) E/flutter (12986):
#17     SembastDatabase.inTransaction (package:sembast/src/database_impl.dart:1238:7) E/flutter (12986): #18 StoreRefMixin.add (package:sembast/src/store_ref_impl.dart:72:25) E/flutter (12986): #19     ParentDao.insert (package:json_serial_test/data/parent_dao.dart:17:24) E/flutter (12986): <asynchronous suspension> E/flutter (12986): #20    
_MyHomePageState.build.<anonymous closure> (package:json_serial_test/main.dart:120:22) E/flutter (12986): #21    
_InkResponseState._handleTap (package:flutter/src/material/ink_well.dart:706:14) E/flutter (12986):
#22     _InkResponseState.build.<anonymous closure> (package:flutter/src/material/ink_well.dart:789:36) E/flutter (12986):
#23     GestureRecognizer.invokeCallback (package:flutter/src/gestures/recognizer.dart:182:24) E/flutter (12986): #24     TapGestureRecognizer.handleTapUp (package:flutter/src/gestures/tap.dart:486:11) E/flutter (12986): #25  BaseTapGestureRecognizer._checkUp (package:flutter/src/gestures/tap.dart:264:5) E/flutter (12986): #26   BaseTapGestureRecognizer.acceptGesture (package:flutter/src/gestures/tap.dart:236:7) E/flutter (12986): #27   GestureArenaManager.sweep (package:flutter/src/gestures/arena.dart:156:27) E/flutter (12986):
#28     GestureBinding.handleEvent (package:flutter/src/gestures/binding.dart:222:20) E/flutter (12986):
#29     GestureBinding.dispatchEvent (package:flutter/src/gestures/binding.dart:198:22) E/flutter (12986):
#30     GestureBinding._handlePointerEvent (package:flutter/src/gestures/binding.dart:156:7) E/flutter (12986):
#31     GestureBinding._flushPointerEventQueue (package:flutter/src/gestures/binding.dart:102:7) E/flutter (12986):
#32     GestureBinding._handlePointerDataPacket (package:flutter/src/gestures/binding.dart:86:7) E/flutter (12986):
#33     _rootRunUnary (dart:async/zone.dart:1138:13) E/flutter (12986): #34     _CustomZone.runUnary (dart:async/zone.dart:1031:19) E/flutter (12986): #35     _CustomZone.runUnaryGuarded (dart:async/zone.dart:933:7) E/flutter (12986): #36     _invoke1 (dart:ui/hooks.dart:273:10) E/flutter (12986): #37    
_dispatchPointerDataPacket (dart:ui/hooks.dart:182:5) E/flutter (12986):

If someone could please assist me in showing what I've missed, done incorrectly, I would greatly appreciate it.

ORIGINAL POST ============== Should json_serializable convert ALL of a class to JSON, or are there limitations that I am hitting?

I have decided to try and resolve a slew of issues I've caused myself by using json_serializable to create the toJson and fromJson methods needed for working with a NoSQL DB.

If I create a class which includes a field that is a List<Ojb>, the generated code seems to result in JSON for each field of the class but fails to do so for the objects in the list.

Simple example

class Parent {
  final int age;
  final String name;
  List<Child> children;

  Parent({this.age, this.name, this.children});

}

class Child {
  final int age;
  final String name;

  Child({this.age, this.name});
}

When I use json_serializable, which seems to work beautifully, I get my toJson and fromJson methods for the above mentioned classes. At first glance, everything looks perfect.

When I attempt to insert a Parent (containing children) into my NoSQL DB, the insert fails to say that the type (referencing the Child object) is not supported - True statement, have no issues with that.

When I step through this via the debugger, here is what I am seeing.

Parent is converted to map age shows up as key with its value the name shows up as key with its value

Up to this point, I can see that everything is a map[.. and all looks great

Then we get to the list of Child objects.

This part is not converted to a map but still exists as a list of Child objects, hence the failure of the insert.

Both classes have the jsonSerializable annotation Both classes are generating the expected code (part of), classes

Everything works perfectly as long as I don't try to use a List<myObject> in one of my classes.

The entire reason one would use a package like json_serializable is to rely on the code that is automatically generated and not have to build it myself. I don't want to have to manually update automatically generated code to resolve this, which is why I have not pasted in the code. If that's the answer, then I'll take a different route.

My question is... is there some configuration that I am missing or possibly doing incorrectly that would allow ALL items within the class to be converted to map/json, even when the fields of a class are not just simple int and string types. I would expect that I could have a class which includes primitives, along with objects, Lists of objects, etc. and everything should generate correctly, or no?

Tillandsia answered 9/3, 2020 at 6:40 Comment(3)
you must you generatetoJson in build.yaml for json serializationPetrillo
Hi Ayush, can you explain a further on what you mean? I do realize I can configure the serializable using the build configuration pubspec.yaml file, but how does this impact the way in which the files are generated?Tillandsia
You did a large edit. Can you confirm that it does not change the question? I.e. that your edit does not invalidate existing answers? I ask because of the size of the edit and the fact that you already have accepted an answer....Dolt
T
35

After many trials and rebuilds, I have resolved this issue and everything appears to be working now with my Sembast DB using json_serializable for my model classes. Possibly there were other factors that contributed to my success, but I think the main difference was adding the following parameter to the JSONSerializable annotation of the Parent class.

explicitToJson: true

Here is the code, in the event it helps somebody else out there.

The full explanation you can find here https://flutter.dev/docs/development/data-and-backend/json

@JsonSerializable(explicitToJson: true)
class Parent {
  int id;
  final String name;
  final int age;
  List<Child> children;
  Job job;

  Parent({this.name, this.age, this.children, this.job});

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

  Map<String, dynamic> toJson() => _$ParentToJson(this);
}
Tillandsia answered 12/3, 2020 at 4:29 Comment(2)
For anyone running into this issue using freezed - see github.com/rrousselGit/freezed/issues/86#issuecomment-593936459Ioab
Thanks! After several hours trying to understand what was wrong with my code, you saved me! Same scenario, complex model with lists. I think this should be actually marked as a bug in the json_serializable plugin, not sure why had to add the explicitToJson: true flagLibbielibbna

© 2022 - 2024 — McMap. All rights reserved.