Search code examples
flutterjsonserializerjsonconvertfreezed

freezed how to assign my own JsonConverter on top level model?


I have freezed model (simplified):

part 'initial_data_model.freezed.dart';
part 'initial_data_model.g.dart';

@freezed
class InitialDataModel with _$InitialDataModel {
  const factory InitialDataModel() = Data;

  const factory InitialDataModel.loading() = Loading;

  const factory InitialDataModel.error([String? message]) = Error;

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

documentation says how to assign custom converters on fields but not on model itself

I got json from backend and somewhere in api_provider I do
return InitialDataModel.fromJson(json);
I have no control on json structure, there aren't "runtimeType" and other stupid redundant things

when I want to create a model from json I call fromJson I'm having this

flutter: CheckedFromJsonException
Could not create `InitialDataModel`.
There is a problem with "runtimeType".
Invalid union type "null"!

ok, again
I have api_provider

final apiProvider = Provider<_ApiProvider>((ref) => _ApiProvider(ref.read));

class _ApiProvider {
  final Reader read;

  _ApiProvider(this.read);

  Future<InitialDataModel> fetchInitialData() async {
    final result = await read(repositoryProvider).send('/initial_data');
    return result.when(
      (json) => InitialDataModel.fromJson(json),
      error: (e) => InitialDataModel.error(e),
    );
  }
}

you may see I'm trying to create InitialDataModel from json

this line throws an error I mentioned above

I don't understand how to create InitialDataModel from json, now in my example it's just empty model, there are no fields

(json) => InitialDataModel.fromJson(json),
json here is Map, it shows an error even if I pass simple empty map {} instead of real json object


Solution

  • The easiest solution is to use the correct constructor instead of _$InitialDataModelFromJson. Example:

    @freezed
    class InitialDataModel with _$InitialDataModel {
      const factory InitialDataModel() = Data;
    
      ...
    
      factory InitialDataModel.fromJson(Map<String, dynamic> json) => Data.fromJson(json);
    }
    

    The drawback of course is that you can only use the fromJson when you're sure you have the correct json, which isn't great. I actually wouldn't recommend this way because it leaves to the caller the burden of checking the validity and calling the correct constructor.


    Another solution, maybe the best, is to follow the documentation and create a custom converter, even though this would require you to have two separated classes.


    Otherwise you could chose a different approach and separate the data class from the union, so you'll have a union used just for the state of the request and a data class for the success response:

    @freezed
    class InitialDataModel with _$InitialDataModel {
      factory InitialDataModel(/* here go your attributes */) = Data;
    
      factory InitialDataModel.fromJson(Map<String, dynamic> json) => _$InitialDataModelFromJson(json);
    }
    
    @freezed
    class Status with _$Status {
      const factory Status.success(InitialDataModel model) = Data;
      const factory Status.loading() = Loading;
      const factory Status.error([String? message]) = Error;
    }
    
    

    and then

    [...]
        return result.when(
          (json) => Status.success(InitialDataModel.fromJson(json)),
          error: (e) => Status.error(e),
        );
    [...]