Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Custom Discriminator Function #368

Closed
jpgilchrist opened this issue Mar 23, 2018 · 18 comments
Closed

Custom Discriminator Function #368

jpgilchrist opened this issue Mar 23, 2018 · 18 comments
Assignees

Comments

@jpgilchrist
Copy link

I have a rather annoying complex data model that I'm trying to map out with built_value. It's a weird case of Polymorphism where an object contains a list of mixed types. These mixed types (like the animal case) are polymorphic, but I don't know which type it should be until I look at an enum type variable on the object. I see there's the concept of the discriminator field which can be used to determine which serializer to use, but I'm not sure that's quite enough in my situation. It'd be nice to take something like a function to switch on the EnumClass (non nullable) and determine what serializer to use for each object in the list.

Dashboard {
  tiles: [ {type: "TYPE1", ...}, {type: "TYPE2", ...} ],
  ...
}

It would be interesting to be able to define this function on the generated class that extends Built.

Here's what I have so far, but I think it fails because it doesn't know which type of Tile to instantiate when it deserializes a Dashboard.

abstract class Dashboard implements Built<Dashboard, DashboardBuilder> {
  static Serializer<Dashboard> get serializer => _$dashboardSerializer;

  int get entity;
  int get id;
  String get name;
  bool get defaultRoomDashboard;
  bool get external;
  String get publicUrlKey;
  bool get subscriptionRequired;
  int get container;
  String get containerName;
  BuiltSet<Tile> get tiles;

  factory Dashboard([updates(DashboardBuilder b)]) = _$Dashboard;
  Dashboard._();
}

@BuiltValue(instantiable: false)
abstract class Tile<T> extends Object with TileViewMixin<T> implements Built<Tile<T>, TileBuilder<T>> {
  int get id;
  TileType get type;
  int get dashboard;
  int get order;
  bool get expanded;
  @override
  T get view;
}

abstract class SavedViewTile extends Object with TileViewMixin<SavedView> implements Built<SavedViewTile, SavedViewTileBuilder> {
  static Serializer<SavedViewTile> get serializer => _$savedViewTileSerializer;

  @override
  SavedView get view;

  factory SavedViewTile([updates(SavedViewTileBuilder b)]) = _$SavedViewTile;
  SavedViewTile._();
}

abstract class WidgetTile extends Object with TileViewMixin<WidgetView> implements Built<WidgetTile, WidgetTileBuilder> {
  static Serializer<WidgetTile> get serializer => _$widgetTileSerializer;

  @override
  WidgetView get view;

  factory WidgetTile([udpates(WidgetTileBuilder b)]) = _$WidgetTile;
  WidgetTile._();
}

abstract class RoomNoteTile extends Object with TileViewMixin<RoomNoteView> implements Built<RoomNoteTile, RoomNoteTileBuilder> {
  static Serializer<RoomNoteTile> get serializer => _$roomNoteTileSerializer;

  @override
  RoomNoteView get view;

  factory RoomNoteTile([udpates(RoomNoteTileBuilder b)]) = _$RoomNoteTile;
  RoomNoteTile._();
}

abstract class TileViewMixin<T> {
  T get view;
}

With my suggested feature improvement you could have something like this for the Tile class.

@BuiltValue(instantiable: false)
abstract class Tile<T> extends Object with TileViewMixin<T> implements Built<Tile<T>, TileBuilder<T>> {
  int get id;
  TileType get type;
  int get dashboard;
  int get order;
  bool get expanded;
  @override
  T get view;

  Serializer get discriminator {
    switch (type) {
      case TileType.SAVED_VIEW:
        return SavedViewTile.serializer;
      case TileType.ROOM_NOTE:
        return RoomNoteTile.serializer;
    }
  }
}

Unless I'm missing something... Furthermore, if this would be better for SO just let me know and I can move it there.

@davidmorgan
Copy link
Collaborator

This is supposed to just work out of the box--a BuiltSet<Tile> should record the type of each element when it serializes, and correctly recreate the right types when it deserializes.

Are you getting the JSON from something other than built_value, maybe? Could you post the error you get when deserializing?

Thanks!

@jpgilchrist
Copy link
Author

jpgilchrist commented Mar 23, 2018

@davidmorgan yes I'm unfortunately trying to deal with an external API that has a data model that is very confusing.

Invalid argument(s): Unknown type on deserialization. Need either specifiedType or discriminator field.

package:built_value/standard_json_plugin.dart 157:7         StandardJsonPlugin._toListUsingDiscriminator
package:built_value/standard_json_plugin.dart 53:16         StandardJsonPlugin.beforeDeserialize
package:built_value/src/built_json_serializers.dart 100:18  BuiltJsonSerializers.deserialize
package:built_value/src/built_json_serializers.dart 132:18  BuiltJsonSerializers._deserialize
package:built_value/src/built_json_serializers.dart 102:18  BuiltJsonSerializers.deserialize
package:built_value/src/built_set_serializer.dart 45:31     BuiltSetSerializer.deserialize.<fn>
dart:_internal                                              ListIterator.moveNext
package:built_collection/src/set/set_builder.dart 56:29     SetBuilder.replace
package:built_value/src/built_set_serializer.dart 44:12     BuiltSetSerializer.deserialize
package:built_value/src/built_json_serializers.dart 139:27  BuiltJsonSerializers._deserialize
package:built_value/src/built_json_serializers.dart 102:18  BuiltJsonSerializers.deserialize
package:veoci/data_model/dashboard_model.g.dart 143:44      _$DashboardSerializer.deserialize
package:built_value/src/built_json_serializers.dart 139:27  BuiltJsonSerializers._deserialize
package:built_value/src/built_json_serializers.dart 102:18  BuiltJsonSerializers.deserialize
package:built_value/src/built_json_serializers.dart 32:12   BuiltJsonSerializers.deserializeWith

here's my unit test that's running the code:

test('dashboard model test', () {
    Map<String, dynamic> json = JSON.decode(TEST_DASHBOARD);
    Dashboard dashboard = _serializers.deserializeWith<Dashboard>(Dashboard.serializer, json);

    expect(dashboard.entity, 8363);

    print(dashboard.tiles.length);
  });

edit:

final Serializers _serializers = (serializers.toBuilder()..addPlugin(new StandardJsonPlugin())).build();

@dave26199
Copy link

dave26199 commented Mar 23, 2018 via email

@jpgilchrist
Copy link
Author

const String TEST_DASHBOARD =  """
{
    "entity": 8363,
    "tiles": [
        {
            "view": {
                "containerId": 3193,
                "urlExtraInfo": "detail?tab=tasks",
                "lastModifiedBy": {
                    "name": "James",
                    "id": 8363
                },
                "aggregates": {},
                "dateModified": "2015-10-20T19:42:09Z",
                "viewId": 431587,
                "groupByColumn": null,
                "count": 10,
                "description": "Test #18598",
                "shared": false,
                "key": "agwrsz",
                "properties": {
                    "originContainerId": "3193",
                    "__widgetType": "SAVED_VIEW",
                    "collapsedContent": "true",
                    "tileColor": "bright-green"
                },
                "state": {
                    "vgrid": {
                        "filters": {},
                        "sortCol": "modified",
                        "sortDir": false,
                        "searchString": "test",
                        "columns": [
                            {
                                "id": "deleteTask",
                                "width": 30
                            },
                            {
                                "id": "status",
                                "hidden": false,
                                "width": 85
                            },
                            {
                                "id": "taskId",
                                "width": 75
                            },
                            {
                                "id": "name",
                                "width": 180
                            },
                            {
                                "id": "taskType",
                                "hidden": false,
                                "width": 180
                            },
                            {
                                "id": "progress",
                                "hidden": false,
                                "width": 105
                            },
                            {
                                "id": "owner",
                                "width": 180
                            },
                            {
                                "id": "dateDue",
                                "width": 150
                            },
                            {
                                "id": "modified",
                                "width": 150
                            },
                            {
                                "id": "lastModifiedBy",
                                "width": 180
                            },
                            {
                                "id": "creator",
                                "width": 180
                            },
                            {
                                "id": "container",
                                "hidden": false,
                                "width": 180
                            }
                        ]
                    },
                    "taskTypeId": null
                },
                "type": "TASK",
                "public": true
            },
            "visible": true,
            "expanded": false,
            "dashboard": 1121,
            "order": 1,
            "id": 4553,
            "type": "SAVED_VIEW"
        },
        {
            "view": {
                "containerId": 3193,
                "urlExtraInfo": "detail?tab=tasks",
                "lastModifiedBy": {
                    "name": "James",
                    "id": 8363
                },
                "aggregates": {},
                "dateModified": "2015-10-20T19:42:57Z",
                "viewId": 431590,
                "groupByColumn": null,
                "count": 1,
                "description": "Test #18598 (testy)",
                "shared": false,
                "key": "qjhpn5",
                "properties": {
                    "originDashboardId": "1322",
                    "__widgetType": "SAVED_VIEW",
                    "originContainerId": "3193",
                    "tileColor": "red"
                },
                "state": {
                    "vgrid": {
                        "filters": {},
                        "sortCol": "modified",
                        "sortDir": false,
                        "searchString": "testy",
                        "columns": [
                            {
                                "id": "deleteTask",
                                "width": 30
                            },
                            {
                                "id": "status",
                                "hidden": false,
                                "width": 85
                            },
                            {
                                "id": "taskId",
                                "width": 75
                            },
                            {
                                "id": "name",
                                "width": 180
                            },
                            {
                                "id": "taskType",
                                "hidden": false,
                                "width": 180
                            },
                            {
                                "id": "progress",
                                "hidden": false,
                                "width": 105
                            },
                            {
                                "id": "owner",
                                "width": 180
                            },
                            {
                                "id": "dateDue",
                                "width": 150
                            },
                            {
                                "id": "modified",
                                "width": 150
                            },
                            {
                                "id": "lastModifiedBy",
                                "width": 180
                            },
                            {
                                "id": "creator",
                                "width": 180
                            },
                            {
                                "id": "container",
                                "hidden": false,
                                "width": 180
                            }
                        ]
                    },
                    "taskTypeId": null
                },
                "type": "TASK",
                "public": true
            },
            "visible": true,
            "expanded": false,
            "dashboard": 1121,
            "order": 2,
            "id": 4555,
            "type": "SAVED_VIEW"
        },
        {
            "view": {
                "fieldNames": {
                    "field_37406": "Date",
                    "field_37417": "Placeholder",
                    "field_37405": "Signature",
                    "field_37504": "Map Area",
                    "field_37416": "Placeholder",
                    "field_37408": "Date and Time",
                    "field_37419": "Room Link",
                    "field_37407": "Time",
                    "field_37418": "Text",
                    "field_37409": "Placeholder",
                    "field_37464": "enter name",
                    "field_37411": "Multi Select",
                    "field_37410": "Which step to follow?",
                    "field_37402": "Text",
                    "field_37501": "Location",
                    "field_37413": "Contact Picker",
                    "field_37412": "Person Picker",
                    "field_37404": "Number",
                    "field_37503": "Multiple Locations",
                    "field_37415": "Task Link",
                    "field_37403": "Custom Embedded Content",
                    "field_37502": "Map Line",
                    "field_37414": "File Attachment"
                },
                "process": {
                    "name": "Release 149",
                    "id": 2325304
                },
                "containerId": 8133,
                "urlExtraInfo": "/processes/2325304",
                "lastModifiedBy": {
                    "name": "Hemi",
                    "id": 7845
                },
                "aggregates": {},
                "dateModified": "2017-03-22T19:12:52Z",
                "viewId": 2337221,
                "groupByColumn": null,
                "count": 7,
                "description": "Workflow",
                "shared": false,
                "key": "2k5j7avy33",
                "properties": {
                    "originDashboardId": "5823",
                    "__widgetType": "SAVED_VIEW",
                    "originContainerId": "8133",
                    "description": "Workflow",
                    "showCalendar": "true"
                },
                "state": {
                    "vgrid": {
                        "filters": {
                            "advancedFilters": {
                                "displayFilters": [],
                                "type": "advanced",
                                "anyAll": "all",
                                "filters": [
                                    {
                                        "columnId": "field_37406",
                                        "condition": "before",
                                        "value": {
                                            "type": "rel",
                                            "value": -3600000,
                                            "relDate": {
                                                "number": "1",
                                                "units": "hours",
                                                "era": "past"
                                            },
                                            "dateOnly": true
                                        },
                                        "type": "date"
                                    }
                                ]
                            }
                        },
                        "sortCol": "lastModified",
                        "sortDir": false,
                        "groupBy": null,
                        "columns": [
                            {
                                "id": "checkedForUpdate",
                                "hidden": false,
                                "width": 30
                            },
                            {
                                "id": "editProcessInvocation",
                                "hidden": false,
                                "width": 40
                            },
                            {
                                "id": "orgSequenceId",
                                "hidden": false,
                                "width": 80
                            },
                            {
                                "id": "requester",
                                "hidden": false,
                                "width": 120
                            },
                            {
                                "id": "currentSteps",
                                "hidden": false,
                                "width": 180
                            },
                            {
                                "id": "previousSteps",
                                "hidden": false,
                                "width": 180
                            },
                            {
                                "id": "wfProgress",
                                "hidden": false,
                                "width": 90
                            },
                            {
                                "id": "created",
                                "hidden": false,
                                "width": 170
                            },
                            {
                                "id": "lastModified",
                                "hidden": false,
                                "width": 170
                            },
                            {
                                "id": "lastModifiedBy",
                                "hidden": false,
                                "width": 120
                            },
                            {
                                "id": "duration",
                                "hidden": false,
                                "width": 120
                            },
                            {
                                "id": "container",
                                "hidden": false,
                                "width": 120
                            },
                            {
                                "id": "stepOwner",
                                "hidden": false,
                                "width": 180
                            },
                            {
                                "id": "field_37402",
                                "hidden": false,
                                "width": 180
                            },
                            {
                                "id": "field_37403",
                                "hidden": false,
                                "width": 180
                            },
                            {
                                "id": "field_37404",
                                "hidden": false,
                                "width": 180
                            },
                            {
                                "id": "field_37405",
                                "hidden": false,
                                "width": 180
                            },
                            {
                                "id": "field_37406",
                                "hidden": false,
                                "width": 180
                            },
                            {
                                "id": "field_37407",
                                "hidden": false,
                                "width": 180
                            },
                            {
                                "id": "field_37408",
                                "hidden": false,
                                "width": 180
                            },
                            {
                                "id": "field_37501",
                                "hidden": false,
                                "width": 180
                            },
                            {
                                "id": "field_37502",
                                "hidden": false,
                                "width": 180
                            },
                            {
                                "id": "field_37503",
                                "hidden": false,
                                "width": 180
                            },
                            {
                                "id": "field_37504",
                                "hidden": false,
                                "width": 180
                            },
                            {
                                "id": "field_37410",
                                "hidden": false,
                                "width": 180
                            },
                            {
                                "id": "field_37411",
                                "hidden": false,
                                "width": 180
                            },
                            {
                                "id": "field_37412",
                                "hidden": false,
                                "width": 180
                            },
                            {
                                "id": "field_37413",
                                "hidden": false,
                                "width": 180
                            },
                            {
                                "id": "field_37464",
                                "hidden": false,
                                "width": 180
                            },
                            {
                                "id": "field_37414",
                                "hidden": false,
                                "width": 180
                            },
                            {
                                "id": "field_37415",
                                "hidden": false,
                                "width": 180
                            },
                            {
                                "id": "field_37418",
                                "hidden": false,
                                "width": 180
                            },
                            {
                                "id": "field_37419",
                                "hidden": false,
                                "width": 180
                            }
                        ],
                        "aggregates": [],
                        "showExactDates": true
                    }
                },
                "type": "INVOCATION",
                "public": true
            },
            "visible": true,
            "expanded": false,
            "dashboard": 1121,
            "order": 3,
            "id": 22663,
            "type": "SAVED_VIEW"
        },
        {
            "view": {
                "createdBy": {
                    "name": null,
                    "id": 7845
                },
                "dateCreated": "2017-03-22T19:30:01Z",
                "properties": {
                    "widgetName": "",
                    "originDashboardId": "5823",
                    "originContainerId": "8133",
                    "center": {
                        "address": {
                            "formattedAddress": "Gujarat 394310, India",
                            "postalCode": "394310",
                            "adminDistrict": "Surat",
                            "countryRegion": "GJ",
                            "country": "IN"
                        },
                        "point": {
                            "coordinates": {
                                "x": 21.1833231,
                                "y": 73.00510550000001
                            }
                        }
                    },
                    "weatherOpts": {
                        "satellite": 1,
                        "temperature": 1,
                        "stormReports": 1,
                        "stormTracks": 1,
                        "severeWarnings": 1,
                        "bulletins": 1
                    },
                    "zoom": "100",
                    "widgetType": "WEATHER"
                },
                "type": "WEATHER"
            },
            "visible": true,
            "expanded": false,
            "dashboard": 1121,
            "order": 4,
            "id": 22664,
            "type": "WIDGET"
        },
        {
            "view": {
                "createdBy": {
                    "name": null,
                    "id": 7845
                },
                "dateCreated": "2017-03-22T19:41:15Z",
                "properties": {
                    "formId": "2325288",
                    "tileLabel": "",
                    "widgetParentContainerId": "1632",
                    "originDashboardId": "5823",
                    "originContainerId": "8133",
                    "formName": "Release 149",
                    "viewKey": "0",
                    "containerId": "8133",
                    "widgetContainerId": "8133"
                },
                "type": "FORM_ENTRY"
            },
            "visible": true,
            "expanded": false,
            "dashboard": 1121,
            "order": 5,
            "id": 22670,
            "type": "WIDGET"
        },
        {
            "view": {
                "createdBy": {
                    "name": null,
                    "id": 38677
                },
                "dateCreated": "2018-02-05T14:52:50Z",
                "properties": {
                    "widgetParentContainerId": "1632",
                    "links": [
                        {
                            "label": "Launch Plan: Plan Creation 11 Jan",
                            "url": "",
                            "linkAction": "LAUNCH_PLAN",
                            "newWindow": false,
                            "container": "9741",
                            "group": 1632,
                            "org": "1",
                            "dynamicContainer": false,
                            "dynamicParentContainer": false,
                            "linkObject": "9769",
                            "plan": {
                                "id": "9769",
                                "name": "Plan Creation 11 Jan",
                                "desc": "By Naveen",
                                "open": false,
                                "mainRoomId": "9770",
                                "mainRoomName": "Room Template Name for Plan Launch",
                                "category": "Testing Plan",
                                "planType": "INCIDENT_RESPONSE",
                                "eventXDate": null,
                                "launchMessage": "Plan Launch Message by naveen for testing",
                                "launchable": true,
                                "progressTracker": null,
                                "subCategory": "Plan Test",
                                "canLaunch": true,
                                "org": "1",
                                "group": "1632"
                            }
                        }
                    ],
                    "widgetContainerId": "9741",
                    "tileName": "Links"
                },
                "type": "LINKS"
            },
            "visible": true,
            "expanded": false,
            "dashboard": 1121,
            "order": 6,
            "id": 27620,
            "type": "WIDGET"
        }
    ]
}
"""

@davidmorgan
Copy link
Collaborator

Thanks.

It should be possible to get something to work using a serializer plugin:

https://github.com/google/built_value.dart/blob/master/built_value/lib/serializer.dart#L141

In the beforeDeserialize method you can checked if specifiedType.root == Tile, and if so, you can modify the JSON. (object will be a Map<String, Object> of JSON values). You should then be able to read the type field and set the discriminator field in a way the serializer understands.

something like:

var map = object as Map;
if (map['type'] == 'SAVED_VIEW') {
  map[r'$'] = 'SavedViewTile';
} ... other cases ...

your aim is to get the $ field, which is the default name for the discriminator field, to match the name of the Dart class that's going to be instantiated.

So what you'd then have is one piece of code for your whole codebase which figures out the discriminator. Once you have this we can try to figure out a reasonable way to encode that in the individual classes instead of in one place.

@jpgilchrist
Copy link
Author

@davidmorgan I was actually looking at the plugin code/documentation the other day and was thinking of doing something like that. Much appreciated, I will let you know when I figure it out 👍

@jpgilchrist
Copy link
Author

jpgilchrist commented Mar 27, 2018

@davidmorgan I wanted to let you know that I was able to make it work with a plugin... here's my plugin code

DiscriminatorPlugin.dart

import 'package:built_value/serializer.dart';
import 'package:veoci/data_model/dashboard_model.dart';

class DiscriminatorPlugin extends SerializerPlugin {
  @override
  Object afterDeserialize(Object object, FullType specifiedType) {
    return object;
  }

  @override
  Object afterSerialize(Object object, FullType specifiedType) {
    return object;
  }

  @override
  Object beforeDeserialize(Object object, FullType specifiedType) {
    if (specifiedType.root == Tile) {
      var map = object as Map;
      var type = TileType.valueOf(map['type'] as String);
      switch (type) {
        case TileType.SAVED_VIEW:
          map[r'$'] = 'SavedViewTile';
          break;
        case TileType.ROOM_NOTE:
          map[r'$'] = 'RoomNoteTile';
          break;
        case TileType.WIDGET:
          map[r'$'] = 'WidgetTile';
          print('WidgetTile $map');
          break;
      }
    } else if (specifiedType.root == WidgetView) {
      var map = object as Map;
      var type = WidgetTileType.valueOf(map['type'] as String);
      var properties = map['properties'] as Map;
      switch (type) {
        case WidgetTileType.WEATHER:
          properties[r'$'] = 'WeatherTileProperties';
          break;
        case WidgetTileType.FORM_ENTRY:
          properties[r'$'] = 'FormEntryTileProperties';
          break;
        case WidgetTileType.LINKS:
          properties[r'$'] = 'LinksTileProperties';
          break;
      }
    } else if (specifiedType.root == Link) {
      var map = object as Map;
      var type = LinkAction.valueOf(map['linkAction']);
      print('Link $type $map');

      switch (type) {
        case LinkAction.LAUNCH_PLAN:
          map[r'$'] = 'LaunchPlanLink';
          break;
      }
    } else {
      print('Discriminator Plugin: ${specifiedType.root} $object');
    }

    return object;
  }

  @override
  Object beforeSerialize(Object object, FullType specifiedType) {
    return object;
  }

}

I wonder if it's possible as apart of the generator to generate a static function on the extension of Built so that it can be called in a plugin. Also, if the FullType knew whether a discriminator was present or not then it could just be called.

@override
beforeDeserialize(Object object, FullType specifiedType) {
  if (specified.root is HasDiscriminator) {
    return specified.root.discriminate(object, type: specifiedType);
  }
  return object;
}

The above is probably wrong, but does describe what would be nice.

@davidmorgan
Copy link
Collaborator

Thanks! I'm glad it worked.

We would need to somehow gather the discriminator information from classes and make it available statically. Not sure how best to do that. I'll give it some thought. I think most likely though this request will hang around until more requests for something similar come in--then we'll have more to go on.

@jpgilchrist
Copy link
Author

@davidmorgan makes complete sense.

@ronlobo
Copy link

ronlobo commented May 14, 2018

Hey,

Pretty much running into the same problem.

Getting BIG INT identities from the wire that needs to be serialized into a concrete implementation of an identity interface.

Thanks @jpgilchrist , I was able to use custom plugin code as well to work around this.

Please checkout the usage example and let me know what you think.

@jeantuffier
Copy link

jeantuffier commented Apr 25, 2019

I'm running in a similar issue, I posted about it on stackoverflow : https://stackoverflow.com/questions/55816076/deserialize-generic-type-with-built-value

I tried to use the plugin solutions without success. Here's what I tried to do :

@override
  Object beforeDeserialize(Object object, FullType specifiedType) {
    var copy = object;
    if (specifiedType.root == ApiData) {
      var map = _toMap(object);
      switch (map["type"]) {
        case "apiComic":
          map[r'$'] = 'ApiData<ApiComic>';
          break;
      }
      copy = _toList(map);
    }

    return copy;
  }

Using object as Map returned me an error saying List<dynamic> is not a subtype of Map.
I created two custom method to cast object from List<dynamic> to Map<String, dynamic> and the other way around. But after the return statement nothing changes, I still have the Unhandled Exception: Deserializing '[offset, 0, limit, 20, total, 2233, count, 20, results, [{...' to 'ApiData' failed due to: Deserializing '[{...' to 'BuiltList' failed due to: Invalid argument(s): Unknown type on deserialization. Need either specifiedType or discriminator field. error. Do you spot what's wrong?

I did a lot of debugging and found out the error come from api_data.g.dart inside deserialize, the case for the generic field expect a parameterT such as this : specifiedType: new FullType(BuiltList, [parameterT])). And that's the problem, parameterT is of type Object because of this line final parameterT = isUnderspecified ? FullType.object : specifiedType.parameters[0]; where specifiedType.parameters[0] is null.

I found the place where the FullType object is created, it's in built_json_serializers.dartin the function T deserializeWith<T>.

I fixed my issue by modifying that method like this :

@override
  T deserializeWith<T>(Serializer<T> serializer, Object serialized) {
    FullType fullType;
    Map map = serialized as Map;
    if(map.containsKey("type")) {
      switch(map["type"]) {
        case "apiComic" :
          fullType = FullType(serializer.types.first, [FullType(ApiComic)]);
          break;
        default:
          fullType = FullType(serializer.types.first);
          break;
      }
    } else {
      fullType = FullType(serializer.types.first);
    }

    return deserialize(serialized, specifiedType: fullType) as T;
  }

and adding this in my serializer.dart:

..addBuilderFactory(
          const FullType(ApiData, const [const FullType(ApiComic)]),
          () => new ApiDataBuilder<ApiComic>()))

That is obviously a temporary hack until I figure out how to fix this issue properly.

in case you want to have a look at the json I receive from the api :

{
	"code": 200,
	"status": "Ok",
	"copyright": "© 2019 MARVEL",
	"attributionText": "Data provided by Marvel. © 2019 MARVEL",
	"attributionHTML": "<a href=\"http://marvel.com\">Data provided by Marvel. © 2019 MARVEL</a>",
	"etag": "b2e767827545e3f07114cbb257598e1aabc11f41",
	"data": {
		"offset": 0,
		"limit": 20,
		"total": 51,
		"count": 20,
		"results": [{
			"id": 116,
			"title": "Acts of Vengeance!",
			"description": "Loki sets about convincing the super-villains of Earth to attack heroes other than those they normally fight in an attempt to destroy the Avengers to absolve his guilt over inadvertently creating the team in the first place.",
			"resourceURI": "http://gateway.marvel.com/v1/public/events/116",
			"urls": [{
					"type": "detail",
					"url": "http://marvel.com/comics/events/116/acts_of_vengeance?utm_campaign=apiRef&utm_source=8d9925577b747f5266703258c321f5ba"
				},
				{
					"type": "wiki",
					"url": "http://marvel.com/universe/Acts_of_Vengeance!?utm_campaign=apiRef&utm_source=8d9925577b747f5266703258c321f5ba"
				}
			],
			"modified": "2013-06-28T16:31:24-0400",
			"start": "1989-12-10 00:00:00",
			"end": "2008-01-04 00:00:00",
			"thumbnail": {
				"path": "http://i.annihil.us/u/prod/marvel/i/mg/9/40/51ca10d996b8b",
				"extension": "jpg"
			},
			"creators": {
				"available": 108,
				"collectionURI": "http://gateway.marvel.com/v1/public/events/116/creators",
				"items": [{
						"resourceURI": "http://gateway.marvel.com/v1/public/creators/2707",
						"name": "Jeff Albrecht",
						"role": "inker"
					},
					{
						"resourceURI": "http://gateway.marvel.com/v1/public/creators/2077",
						"name": "Hilary Barta",
						"role": "inker"
					}
				],
				"returned": 2
			},
			"characters": {
				"available": 102,
				"collectionURI": "http://gateway.marvel.com/v1/public/events/116/characters",
				"items": [{
						"resourceURI": "http://gateway.marvel.com/v1/public/characters/1009435",
						"name": "Alicia Masters"
					},
					{
						"resourceURI": "http://gateway.marvel.com/v1/public/characters/1010370",
						"name": "Alpha Flight"
					}
				],
				"returned": 2
			},
			"stories": {
				"available": 144,
				"collectionURI": "http://gateway.marvel.com/v1/public/events/116/stories",
				"items": [{
						"resourceURI": "http://gateway.marvel.com/v1/public/stories/12960",
						"name": "Fantastic Four (1961) #334",
						"type": "cover"
					},
					{
						"resourceURI": "http://gateway.marvel.com/v1/public/stories/12961",
						"name": "Shadows of Alarm..!",
						"type": "interiorStory"
					}
				],
				"returned": 2
			},
			"comics": {
				"available": 52,
				"collectionURI": "http://gateway.marvel.com/v1/public/events/116/comics",
				"items": [{
						"resourceURI": "http://gateway.marvel.com/v1/public/comics/12744",
						"name": "Alpha Flight (1983) #79"
					},
					{
						"resourceURI": "http://gateway.marvel.com/v1/public/comics/12746",
						"name": "Alpha Flight (1983) #80"
					}
				],
				"returned": 2
			},
			"series": {
				"available": 22,
				"collectionURI": "http://gateway.marvel.com/v1/public/events/116/series",
				"items": [{
						"resourceURI": "http://gateway.marvel.com/v1/public/series/2116",
						"name": "Alpha Flight (1983 - 1994)"
					},
					{
						"resourceURI": "http://gateway.marvel.com/v1/public/series/1991",
						"name": "Avengers (1963 - 1996)"
					}
				],
				"returned": 2
			},
			"next": {
				"resourceURI": "http://gateway.marvel.com/v1/public/events/240",
				"name": "Days of Future Present"
			},
			"previous": {
				"resourceURI": "http://gateway.marvel.com/v1/public/events/233",
				"name": "Atlantis Attacks"
			}
		}]
	}
}

In the end I think deserializing generic list is pretty common when dealing with APIs. It would be great to have a written doc about how to do that properly. I haven't found anything online dealing with this

@hasan314
Copy link

Hi @jeantuffier , I am having a similar issue and I tried to use your workaround but I believe I am messing up somewhere, would you mind helping out? I can share more details.

@deadsoul44
Copy link

Generics and type inference is a must. Otherwise, we have to write a lot of boilerplate code. I hope it will be implemented very soon.

@jeantuffier
Copy link

@hasan314 I ended up switching to another library that makes it a bit simpler to handle my case.
https://pub.dev/packages/json_serializable

@davidmorgan
Copy link
Collaborator

You can specify the discriminator field--the field in the JSON which should contain the type name--as an argument to StandardJsonPlugin

https://pub.dev/documentation/built_value/latest/standard_json_plugin/StandardJsonPlugin/StandardJsonPlugin.html

Then, if the string in the JSON does not match your class names, you need to use @BuiltValue(wireName: '<name used in JSON>') on each class. e.g.

@BuiltValue(wireName: 'apiComic')
abstract class ApiComic ...

built_value already supports generics and polymorphism out of the box--it's only when you need to work with someone else's JSON API that you might need to do something custom. Unfortunately, there's no one standard for how to represent types in JSON, so there's no way it can just work out of the box for everyone.

@jeantuffier
Copy link

I guess I missed the part talking about @BuiltValue(wireName: 'apiComic') in the doc ! But like I said I did move to another library and made some more changes into my code a while ago.
Still, thanks for the answer @davidmorgan :)

@ayushbagaria17
Copy link

ayushbagaria17 commented Jun 20, 2020

Hi, I have a generic class

abstract class Response<T> implements Built<Response<T>, ResponseBuilder<T>> { int get code; @nullable T get data; String get status; @nullable List<CommonErrorPayload> get error; Response._(); factory Response([void Function(ResponseBuilder<T>) updates]) = _$Response<T>; static Serializer<Response<Object>> get serializer => _$responseSerializer; }

and response like
{status: OK, code: 200, data: {userId: id, userName: User Name, emailId: null, refferFrom: null, signupState: 3, isDeleted: 0, isBlocked: 0, addTimeStamp: 1590384432, deleteDate: null, phoneNumber: 1234567890}}

I am getting this error
Unhandled Exception: Unhandled error type '_$ResponseSerializer' is not a subtype of type 'Serializer<Response<User>>'

I went through all the above comments but couldn't understand how to fix it.
Is it a bug or I am missing something?

@davidmorgan
Copy link
Collaborator

Try Serializer<Response> instead of Serializer<Response<Object>>, does that work?

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

No branches or pull requests

8 participants