google / google/json_serializable.dart
Generic `JsonConverter<T>` is not applied to `List<T>` fields
- Dominant language
- Dart
- Stars
- 1.6k
- Forks
- 461
- Avg merge
- 45m
- Merged PRs (30d)
- 1
Description
## Description
A generic `JsonConverter` targeting `List?` 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` 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
```dart
import 'package:json_annotation/json_annotation.dart';
class SafeListConverter implements JsonConverter?, Object?> {
final T Function(Map json) fromJsonFactory;
final Object? Function(T value) toJsonFactory;
const SafeListConverter(this.fromJsonFactory, this.toJsonFactory);
@override
List? fromJson(Object? json) {
print('CONVERTER CALLED: ${json.runtimeType}');
if (json is Map) {
return [fromJsonFactory(json)];
}
if (json is List) {
return json
.whereType>()
.map(fromJsonFactory)
.toList();
}
return null;
}
@override
Object? toJson(List? object) {
if (object == null) return null;
if (object.isEmpty) return '';
if (object.length == 1) {
return toJsonFactory(object.first);
}
return object.map(toJsonFactory).toList();
}
}
```
## Model
```dart
import 'package:json_annotation/json_annotation.dart';
part 'model.g.dart';
@JsonSerializable()
class Response {
@SafeListConverter(Product.fromJson, _productToJson)
final List? products;
const Response({this.products});
factory Response.fromJson(Map json) => _$ResponseFromJson(json);
Map toJson() => _$ResponseToJson(this);
}
@JsonSerializable()
class Product {
final int id;
final String name;
const Product({required this.id, required this.name});
factory Product.fromJson(Map json) => _$ProductFromJson(json);
Map 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:
```dart
Response _$ResponseFromJson(Map json) => Response(
products: (json['products'] as List?)
?.map((e) => Product.fromJson(e as Map))
.toList(),
);
```
The `SafeListConverter` 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:
```json
{
"products": {
"id": 1,
"name": "iPhone"
}
}
```
instead of:
```json
{
"products": [
{
"id": 1,
"name": "iPhone"
}
]
}
```
Because the generated code performs:
```dart
json['products'] as List?
```
the following exception occurs:
```text
_TypeError:
type '_Map' is not a subtype of type 'List?'
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:
```dart
class SafeProductListConverter implements JsonConverter?, Object?> {
const SafeProductListConverter();
@override
List? fromJson(Object? json) {
print('>>> CONVERTER CALLED: ${json.runtimeType}');
if (json is Map) {
return [Product.fromJson(json)];
}
if (json is List) {
return json
.whereType>()
.map(Product.fromJson)
.toList();
}
return null;
}
@override
Object? toJson(List? 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:
```dart
@SafeProductListConverter()
final List? products;
```
With this converter, the generated code correctly invokes the converter and the runtime tests pass.
The output confirms:
```text
>>> CONVERTER CALLED: _Map
>>> CONVERTER CALLED: List>
>>> CONVERTER CALLED: Null
>>> CONVERTER CALLED: String
```
Therefore, the issue appears to be specifically related to the generic converter:
```dart
JsonConverter?, 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?, Object?>` matches the field type signature `List?`, and invoke the converter directly:
Given:
```dart
@SafeListConverter(
Product.fromJson,
_productToJson,
)
final List? products;
```
I would expect generated code to invoke the converter, approximately:
```dart
products: const SafeListConverter(
Product.fromJson,
_productToJson,
).fromJson(json['products']),
```
so that the converter can handle both:
```json
{
"products": {
"id": 1,
"name": "iPhone"
}
}
```
and:
```json
{
"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.
Contributor guide
Research direction
Reproduce the example using model.dart and inspect the generated model.g.dart output, comparing the generic and non-generic converter cases. The issue is done when the generic converter is invoked for both object and list JSON inputs, or when the supported limitation is documented with a recommended pattern.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- dart
- Domain
- build-system, tooling
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 58/100