Существует ли идиоматический способ разбора JSON на объекты с общим базовым классом, но разными формами данных?

#dart

#dart

Вопрос:

Я совершенно новичок в Dart, поэтому я только что начал внедрять SDK для существующего API, но я столкнулся с некоторыми проблемами, выясняя, как структурировать вещи, чтобы сократить шаблонность, когда я анализирую ответы с сервера.

В принципе, все ответы от API выглядят следующим образом:

 {
  "data": {
    ...
  },
  "error": {
    ...
  }
}
 

где присутствует либо data или error , но никогда оба. Довольно стандартный материал. Форма
error не так уж интересна, поскольку она одинакова для каждого ответа, но data объект
отличается для каждого ответа. До сих пор мне удавалось получить что-то «рабочее», объявив базовый класс
, который обрабатывает ошибку и передает ее super (я должен отметить, что я использую
json_serializer пакет):

 class Response {
  Response({this.error});

  Error? error;
}

@JsonSerializable(createToJson: false)
class ExampleResponse extends Response {
  ExampleResponse({
    this.data,
    Error? error,
  }) : super(error: error);

  Map<String, dynamic>? data;

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

Это работает, но я теряю всю информацию о типе data поля. Я не уверен, как подойти к этому
отсюда, чтобы не использовать еще один класс для формы данных:

 @JsonSerializable()
class Thing {
  Thing(this.name);

  String name;

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

  String toString() => 'Thing{name: $name}';
}

@JsonSerializable()
class ExampleResponseData {
  ExampleResponseData(this.thing);

  Thing thing;

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

  String toString() => 'ExampleResponseData{thing: $thing}';
}

@JsonSerializable(createToJson: false)
class ExampleResponse extends Response {
  ExampleResponse({
    this.data,
    Error? error,
  }) : super(error: error);

  ExampleResponseData? data;

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

  String toString() => 'ExampleResponse{error: $error, data: $data}';
}
 

В Go я бы просто определил внутреннюю структуру для случаев, когда тип не имеет значения вне
синтаксического анализа JSON, например:

 type ExampleResponse struct {
    Error *Error `json:"error"`
    Data *struct {
        Thing Thing `json:"thing"`
    } `json:"thing"`
}
 

но я намного лучше разбираюсь в ООП в целом и на еще более незнакомой почве с Dart. Есть ли
лучший способ справиться с этим, чтобы в итоге не было в два раза больше типов, чем ответов?

Комментарии:

1. Является ли json_serializable строгим требованием для вас, или вы можете рассмотреть другие варианты, такие как pub.dev/packages/built_value ?

2. Я бы сказал, что на данный момент у меня нет никаких строгих требований, кроме формы ответа API. Когда я изначально рассматривал обе библиотеки, built_value они казались более шаблонными, но у меня недостаточно понимания того, будет ли это лучше или хуже, учитывая мою текущую проблему.

3. Будет ли у вас другой тип «данных» на одной и той же конечной точке, или вы будете знать, какой тип данных ожидать на данной конечной точке?

4. Конечные точки всегда возвращают одну и ту же форму данных. В общих чертах API разработан как псевдо-RPC-сервис, где каждая конечная точка имеет известный формат запроса и ответа, но все ответы преобразуются идентичным образом. Возврат ошибок преобразуется и используется для заполнения error поля, возврат без ошибок помещается под data поле, но в остальном остается неизменным. Итак, в приведенном выше примере вызов вымышленной /Example конечной точки всегда возвращает a Thing , который маршалируется в thing поле внутри data объекта.

Ответ №1:

Вы можете найти решение на основе built_value . Идея состоит в том, чтобы использовать generic для типа данных. Встроенное значение может обрабатывать такие случаи, хотя у вас будет немного шаблонности. Ниже вы можете найти пример:

 
import 'package:built_value/built_value.dart';
import 'package:built_value/serializer.dart';
import 'package:built_value/standard_json_plugin.dart';

part 'built_value_example.g.dart';

void main(List<String> arguments) {
  final resultAJson = endpointA();
  final resultA = serializers.deserialize(resultAJson, specifiedType: FullType(Response, [FullType(DataA)]));
  print(resultA);

  final resultBJson = endpointB();
  final resultB = serializers.deserialize(resultBJson, specifiedType: FullType(Response, [FullType(DataB)]));
  print(resultB);
}

Map<String, dynamic> endpointA() {
  final data = DataA((b) => b..dataAField = 7);
  final response = Response<DataA>((b) => b..data = data);
  return serializers.serialize(response, specifiedType: FullType(Response, [FullType(DataA)])) as Map<String, dynamic>;
}

Map<String, dynamic> endpointB() {
  final data = DataB((b) => b..dataBField = 'data b value');
  final response = Response<DataB>((b) => b..data = data..error = 'data b error');
  return serializers.serialize(response, specifiedType: FullType(Response, [FullType(DataB)])) as Map<String, dynamic>;
}

abstract class Response<DATA_TYPE> implements Built<Response<DATA_TYPE>, ResponseBuilder<DATA_TYPE>> {
  Response._();

  factory Response([Function(ResponseBuilder<DATA_TYPE> b) updates]) = _$Response<DATA_TYPE>;

  static Serializer<Response> get serializer => _$responseSerializer;

  DATA_TYPE get data;

  String? get error;
}

abstract class DataA implements Built<DataA, DataABuilder> {
  DataA._();

  factory DataA([Function(DataABuilder b) updates]) = _$DataA;

  static Serializer<DataA> get serializer => _$dataASerializer;

  int get dataAField;
}

abstract class DataB implements Built<DataB, DataBBuilder> {
  DataB._();

  factory DataB([Function(DataBBuilder b) updates]) = _$DataB;

  static Serializer<DataB> get serializer => _$dataBSerializer;

  String get dataBField;
}

@SerializersFor([
  DataA,
  DataB,
  Response,
])
final Serializers serializers = (_$serializers.toBuilder()
      ..addPlugin(StandardJsonPlugin())
      ..addBuilderFactory(FullType(Response, [FullType(DataA)]), () => ResponseBuilder<DataA>())
      ..addBuilderFactory(FullType(Response, [FullType(DataB)]), () => ResponseBuilder<DataB>()))
    .build();


 

Комментарии:

1. Большое вам спасибо. Мне нужно будет найти время, чтобы разобраться, чтобы лучше понять, как все это работает, но я ценю, что вы нашли время.