Description
A generic JsonConverter targeting List<T>? is not picked up by json_serializable, even when the generic type parameter is explicitly specified on the annotation.
Instead, json_serializable generates the default List<dynamic> cast and completely ignores the converter.
This causes a runtime TypeError when the API returns a single JSON object instead of an array.
Reproduction
Environment
- Flutter version:
3.44.9
- Dart version:
3.12.2
- json_serializable:
6.14.1
- json_annotation:
4.12.0
- build_runner:
2.15.1
- OS:
Win 11
Generic converter
import 'package:json_annotation/json_annotation.dart';
class SafeListConverter<T> implements JsonConverter<List<T>?, Object?> {
final T Function(Map<String, dynamic> json) fromJsonFactory;
final Object? Function(T value) toJsonFactory;
const SafeListConverter(this.fromJsonFactory, this.toJsonFactory);
@override
List<T>? fromJson(Object? json) {
print('CONVERTER CALLED: ${json.runtimeType}');
if (json is Map<String, dynamic>) {
return [fromJsonFactory(json)];
}
if (json is List) {
return json
.whereType<Map<String, dynamic>>()
.map(fromJsonFactory)
.toList();
}
return null;
}
@override
Object? toJson(List<T>? object) {
if (object == null) return null;
if (object.isEmpty) return '';
if (object.length == 1) {
return toJsonFactory(object.first);
}
return object.map(toJsonFactory).toList();
}
}
Model
import 'package:json_annotation/json_annotation.dart';
part 'model.g.dart';
@JsonSerializable()
class Response {
@SafeListConverter<Product>(Product.fromJson, _productToJson)
final List<Product>? products;
const Response({this.products});
factory Response.fromJson(Map<String, dynamic> json) => _$ResponseFromJson(json);
Map<String, dynamic> toJson() => _$ResponseToJson(this);
}
@JsonSerializable()
class Product {
final int id;
final String name;
const Product({required this.id, required this.name});
factory Product.fromJson(Map<String, dynamic> json) => _$ProductFromJson(json);
Map<String, dynamic> toJson() => _$ProductToJson(this);
}
Object? _productToJson(Product product) {
return product.toJson();
}
Generated code
I expected the converter to be applied to the products field.
Instead, json_serializable generates:
Response _$ResponseFromJson(Map<String, dynamic> json) => Response(
products: (json['products'] as List<dynamic>?)
?.map((e) => Product.fromJson(e as Map<String, dynamic>))
.toList(),
);
The SafeListConverter<Product> is completely omitted.
The same generated output was observed with json_serializable 6.13.0.
Runtime failure
The API I'm integrating with has an inconsistent response format.
For one item it returns:
{
"products": {
"id": 1,
"name": "iPhone"
}
}
instead of:
{
"products": [
{
"id": 1,
"name": "iPhone"
}
]
}
Because the generated code performs:
json['products'] as List<dynamic>?
the following exception occurs:
_TypeError:
type '_Map<String, dynamic>' is not a subtype of type 'List<dynamic>?'
in type cast
The important point is that SafeListConverter.fromJson() is never called.
Control test
I tested the same scenario with a non-generic converter:
class SafeProductListConverter implements JsonConverter<List<Product>?, Object?> {
const SafeProductListConverter();
@override
List<Product>? fromJson(Object? json) {
print('>>> CONVERTER CALLED: ${json.runtimeType}');
if (json is Map<String, dynamic>) {
return [Product.fromJson(json)];
}
if (json is List) {
return json
.whereType<Map<String, dynamic>>()
.map(Product.fromJson)
.toList();
}
return null;
}
@override
Object? toJson(List<Product>? object) {
if (object == null) return null;
if (object.isEmpty) return '';
if (object.length == 1) {
return object.first.toJson();
}
return object.map((e) => e.toJson()).toList();
}
}
Used as:
@SafeProductListConverter()
final List<Product>? products;
With this converter, the generated code correctly invokes the converter and the runtime tests pass.
The output confirms:
>>> CONVERTER CALLED: _Map<String, Object>
>>> CONVERTER CALLED: List<Map<String, Object>>
>>> CONVERTER CALLED: Null
>>> CONVERTER CALLED: String
Therefore, the issue appears to be specifically related to the generic converter:
JsonConverter<List<T>?, Object?>
rather than JsonConverter support in general.
Expected behavior
The generator should evaluate the assigned constructor metadata, resolve the generic substitution (T -> Product), realize that JsonConverter<List<Product>?, Object?> matches the field type signature List<Product>?, and invoke the converter directly:
Given:
@SafeListConverter<Product>(
Product.fromJson,
_productToJson,
)
final List<Product>? products;
I would expect generated code to invoke the converter, approximately:
products: const SafeListConverter<Product>(
Product.fromJson,
_productToJson,
).fromJson(json['products']),
so that the converter can handle both:
{
"products": {
"id": 1,
"name": "iPhone"
}
}
and:
{
"products": [
{
"id": 1,
"name": "iPhone"
}
]
}
If this pattern is not supported, it would be helpful if the documentation could clarify the limitation or recommend the supported pattern for reusable generic list converters.
If it is expected to work, this appears to be a type-matching issue in code generation.
Description
A generic
JsonConvertertargetingList<T>?is not picked up byjson_serializable, even when the generic type parameter is explicitly specified on the annotation.Instead,
json_serializablegenerates the defaultList<dynamic>cast and completely ignores the converter.This causes a runtime
TypeErrorwhen the API returns a single JSON object instead of an array.Reproduction
Environment
3.44.93.12.26.14.14.12.02.15.1Win 11Generic converter
Model
Generated code
I expected the converter to be applied to the
productsfield.Instead,
json_serializablegenerates:The
SafeListConverter<Product>is completely omitted.The same generated output was observed with
json_serializable 6.13.0.Runtime failure
The API I'm integrating with has an inconsistent response format.
For one item it returns:
{ "products": { "id": 1, "name": "iPhone" } }instead of:
{ "products": [ { "id": 1, "name": "iPhone" } ] }Because the generated code performs:
the following exception occurs:
The important point is that
SafeListConverter.fromJson()is never called.Control test
I tested the same scenario with a non-generic converter:
Used as:
With this converter, the generated code correctly invokes the converter and the runtime tests pass.
The output confirms:
Therefore, the issue appears to be specifically related to the generic converter:
rather than
JsonConvertersupport in general.Expected behavior
The generator should evaluate the assigned constructor metadata, resolve the generic substitution (
T->Product), realize thatJsonConverter<List<Product>?, Object?>matches the field type signatureList<Product>?, and invoke the converter directly:Given:
I would expect generated code to invoke the converter, approximately:
so that the converter can handle both:
{ "products": { "id": 1, "name": "iPhone" } }and:
{ "products": [ { "id": 1, "name": "iPhone" } ] }If this pattern is not supported, it would be helpful if the documentation could clarify the limitation or recommend the supported pattern for reusable generic list converters.
If it is expected to work, this appears to be a type-matching issue in code generation.