schultek / dart_mappable

Improved json serialization and data classes with full support for generics, inheritance, customization and more.
https://pub.dev/packages/dart_mappable
MIT License
153 stars 23 forks source link

Mapping deep object nesting using Map with int key #132

Open murnikov opened 12 months ago

murnikov commented 12 months ago

My use case calls for an object hierarchy 3 levels deep. Extremely simplified example below

import 'package:flutter/material.dart';
import 'package:dart_mappable/dart_mappable.dart';

part 'main.mapper.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    // Matryoshka granddaughter = const Matryoshka('1', 'Granddaughter', null);
    Matryoshka daughter = const Matryoshka(2, 'Daughter', null);
    Matryoshka granny = Matryoshka(3, 'Granny', {daughter.id: daughter});
    print('original ${granny.toString()}');
    Matryoshka clone = Matryoshka.fromJson(granny.toJson());
    print('clone ${clone.toString()}');
    return Scaffold(
      appBar: AppBar(
        title: const Text('Title'),
      ),
      body: const Center(
        child: Text('whatever'),
      ),
    );
  }
}

@MappableClass()
class Matryoshka with MatryoshkaMappable {
  const Matryoshka(this.id, this.name, this.children);
  final int id;
  final String name;
  @MappableField(hook: MappableHookNullableMap())
  final Map<int, Matryoshka>? children;
  static const fromMap = MatryoshkaMapper.fromMap;
  static const fromJson = MatryoshkaMapper.fromJson;
}

class MappableHookNullableMap extends MappingHook {
  const MappableHookNullableMap();
  @override
  Object? beforeEncode(Object? value) {
    if (value == null) {
      return null;
    } else if (value is Map<int, dynamic>) {
      return value.map((key, value) => MapEntry(key.toString(), value));
    } else {
      return value;
    }
  }

  @override
  Object? beforeDecode(Object? value) {
    if (value == null) {
      return null;
    } else if (value is List<dynamic> && value.isEmpty) {
      return null;
    } else if (value is Map<String, dynamic>) {
      return value.map((key, value) => MapEntry(int.parse(key), value));
    } else {
      return value;
    }
  }
}

Running above code results in

Restarted application in 1 701ms.
I/flutter (23960): original Matryoshka(id: 3, name: Granny, children: {2: Matryoshka(id: 2, name: Daughter, children: null)})

════════ Exception caught by widgets library ═══════════════════════════════════
The following _ChainedMapperException was thrown building MyApp(dirty):
MapperException: Failed to decode (Matryoshka).children(Map<int, Matryoshka>?).value(Matryoshka): Expected a value of type Map<String, dynamic>, but got type String.

The relevant error-causing widget was
MyApp
When the exception was thrown, this was the stack
#0      TypeCheck.checked
#1      ClassMapperBase.decode
#2      InterfaceMapperBase.decoder
#3      ClassMapperBase.decoder
#4      MapperBase.decodeValue
#5      _MapperContainerBase.fromValue
#6      DecodingUtil.$dec
#7      _MapDecoder2._decode.<anonymous closure>
#8      MapBase.map (dart:collection/maps.dart:82:28)
#9      _MapDecoder2._decode
#10     ResolvedType._type
#11     ResolvedType.provideTo
#12     TypePlus.provideTo
#13     _MapDecoder2.decode
#14     _MapDecoder._decode
#15     ResolvedType._type
#16     ResolvedType.provideTo
#17     TypePlus.provideTo
#18     _MapDecoder.decode
#19     MapMapper.decoder
#20     MapperBase.decodeValue
#21     _MapperContainerBase.fromValue
#22     DecodingUtil.$dec
#23     Field.decode
#24     DecodingData.dec
#25     MatryoshkaMapper._instantiate
#26     InterfaceMapperBase.decode
#27     ClassMapperBase.decode
#28     InterfaceMapperBase.decoder
#29     ClassMapperBase.decoder
#30     MapperBase.decodeValue
#31     InterfaceMapperBase.decodeJson
#32     MatryoshkaMapper.fromJson
#33     MyApp.build
#34     StatelessElement.build
#35     ComponentElement.performRebuild
#36     Element.rebuild
#37     ComponentElement._firstBuild
#38     ComponentElement.mount
...     Normal element mounting (27 frames)
#65     Element.inflateWidget
#66     Element.updateChild
#67     RenderObjectToWidgetElement._rebuild
#68     RenderObjectToWidgetElement.mount
#69     RenderObjectToWidgetAdapter.attachToRenderTree.<anonymous closure>
#70     BuildOwner.buildScope
#71     RenderObjectToWidgetAdapter.attachToRenderTree
#72     WidgetsBinding.attachRootWidget
#73     WidgetsBinding.scheduleAttachRootWidget.<anonymous closure>
#77     _RawReceivePort._handleMessage (dart:isolate-patch/isolate_patch.dart:189:12)
(elided 3 frames from class _Timer and dart:async-patch)
schultek commented 12 months ago

Try using 'afterEncode' instead of 'beforeEncode' in the hook.