google / google/json_serializable.dart

Generate Type-Safe Field Name Accessors

Open
#1,543 5 comments 0 reactions 0 assignees View on GitHub
Dominant language
Dart
Stars
1.6k
Forks
461
Avg merge
45m
Merged PRs (30d)
1

Description

### The problem

`json_serializable` is great for converting data to Dart models for something like Firebase which operates in JSON data directly. This makes it type-safe using `.withConverter` methods on Firebase collection and document references in the client side SDK. However, when it comes to partially updating data, say a single field in the collection, we have use strings directly for field names in JSON. This prone to typo errors and refactors including renaming of the field.

For example, this simple
```dart
@JsonSerializable()
class User {
final String name;
final int age;
final bool isActive;

User({required this.name, required this.age, required this.isActive});

factory User.fromJson(Map json) => _$UserFromJson(json);

Map toJson() => _$UserToJson(this);
}
```

Updating the whole model is fine like:

```dart
ref.set(user.toJson());

// or

ref.set(user); // withConverter
```

However, for partial updates,

```dart
ref.update('isActive': false); // <-- using string as field name

// or

ref.update('is_active': false); // <-- using string as field name when field rename is set to snake case!
```

### Proposal

What if the package generates these bindings along with serialization and deserialization code.

For example, what if you could do,
```dart
ref.update(User.fields.isActive, false); // <-- uses safe generated code
```

What would the generated code be like,
```dart
final class $UserFields {

const $UserFields();

final String name = 'name';
final String age = 'age';
final String active = 'active';
}
```

This is how we can expose it through the data model:
```dart
@JsonSerializable(generateFieldsClass: true)
class User {

// ...
static const $UserFields fields = $UserFields();

}
```

I am aware of `createFieldMap: true` option in `@JsonSerializable()` but that generates a `Map`. This is not much useful for scenarios like this one.

### Supporting Nested structures

The above approach only works and works well for Level 1 or flat structors. If the User model had a field with a custom serializable type, then it wouldn't work.

In these cases, we often need a nested path. For example, Firebase supports paths with dot separator `(.)` for updating nested structures. We could achieve something similar by using `extension types` as shown below.

For being able to do the following:

```dart
void main() {
print(User.fields.name); // Outputs: name
print(User.fields.address.city); // Outputs: address.city
print(User.fields.address.zipCode); // Outputs: user.address.zipCode
print(Address.fields.street); // Outputs: street
}
```

We need this code:
```dart
@JsonSerializable()
class User {
final String name;
final int age;
final bool active;
final Address address;

User({required this.name, required this.age, required this.active, required this.address});

factory User.fromJson(Map json) => _$UserFromJson(json);

Map toJson() => _$UserToJson(this);

static const $UserFields fields = $UserFields();
}

@JsonSerializable()
class Address {
final String street;
final String city;
final String zipCode;

Address({required this.street, required this.city, required this.zipCode});

factory Address.fromJson(Map json) => _$AddressFromJson(json);

Map toJson() => _$AddressToJson(this);

static const $AddressFields fields = $AddressFields();
}

extension type const $UserFields.prefixed(String prefix) implements String {
const $UserFields() : prefix = '';

String _join(String field) => prefix.isEmpty ? field : '$prefix.$field';

String get name => _join('name');

String get age => _join('age');

String get active => _join('active');

$AddressFields get address => $AddressFields.prefixed(_join('address'));
}

extension type const $AddressFields.prefixed(String prefix) implements String {
const $AddressFields() : prefix = '';

String _join(String field) => prefix.isEmpty ? field : '$prefix.$field';

String get street => _join('street');

String get city => _join('city');

String get zipCode => _join('zipCode');
}
```

This can work really well for any level of nesting. This is just for demonstration, for the full feature, perhaps a separator can be configurable as well.

Now, this might look a lot and could be out of scope for this package, but it is easier to do with this package only. If someone were to implement it separately then they need to account for `json_serializable`'s configuration options that changes field name casing.

Thoughts?

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.