google / google/json_serializable.dart

Generic `JsonConverter<T>` is not applied to `List<T>` fields

Open
#1,589 0 comments 0 reactions 0 assignees View on GitHub
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.