-
Notifications
You must be signed in to change notification settings - Fork 111
Description
Short version: I've written a builder to generate ID types for a project that also uses json_serializable. My builder works fine, but json_serializable fails because it cannot resolve the types generated by my builder.
So in our project, we use extension types to define explicit ID types for models, like this:
extension type const UserId(int id) implements Object {
factory UserId.fromJson(int id) => UserId(id);
int toJson() => id;
static UserId? tryParse(String? value) {
final id = int.tryParse(value ?? '');
return id == null ? null : UserId(id);
}
}
We have a lot of these, so I decided to try to start generating them for improved dev experience and maintenance (also out of curiosity to try using source_gen). So I came up with a pretty simple builder which basically just outputs that code with the type name substituted. And it works fine! Very satisfying. However, when I have a model that has an @IdType
and @JsonSerializable
, json_serializable fails:
E json_serializable on lib/models/user.dart:
Could not generate `fromJson` code for `id`.
To support the type `InvalidType` you can:
* Use `JsonConverter`
https://pub.dev/documentation/json_annotation/latest/json_annotation/JsonConverter-class.html
* Use `JsonKey` fields `fromJson` and `toJson`
https://pub.dev/documentation/json_annotation/latest/json_annotation/JsonKey/fromJson.html
https://pub.dev/documentation/json_annotation/latest/json_annotation/JsonKey/toJson.html
package:impruvon_health/models/user.dart:21:16
╷
21 │ final UserId id;
To be clear, the id types are usable by json_serializable without any issues when I define them by hand in the model file. It seems like json_serializable is not able to use intermediate generated parts from before the combining builder combines all the parts. It kind of makes sense that it's like this, but this is definitely a limitation given that it essentially bars you from writing any custom builders that need to interact with other custom builders, which is probably a lot of use cases.
fwiw I also tried forking json_serializable and adding required_inputs: ['id_type.g.part']
to its build.yaml
but that also didn't work. (this isn't actually a viable solution because we don't want to maintain a fork, just did this to try to understand the problem a bit more)
So:
A) Is there any reasonable workaround for this? I'm not enthusiastic about generating into some .id.dart
file (and I'm not even sure that would work), would rather keep everything in {model}.g.dart
. I suppose generating them all to some models/ids.g.dart
would be viable but don't love it either.
B) Is there any improvement that could be made to source_gen (or build?) that could prevent this from being an issue? I would be happy to work on this but probably wouldn't know where to start.
Some extra context:
build.yaml from my builder package
builders:
impruvon_builder_gen:
target: ":impruvon_builder_gen"
import: "package:impruvon_builder_gen/builder.dart"
builder_factories: ["idTypeBuilder"]
build_extensions: {".dart": ["id_type.g.part"]}
auto_apply: dependents
build_to: cache
applies_builders: ["source_gen:combining_builder"]
runs_before: ["json_serializable"]
builder.dart
import 'package:build/build.dart';
import 'package:impruvon_builder_gen/id_type_generator.dart';
import 'package:source_gen/source_gen.dart' show SharedPartBuilder;
Builder idTypeBuilder(BuilderOptions options) =>
SharedPartBuilder([IdTypeGenerator(options.config)], 'id_type');
id_type_generator.dart
import 'package:analyzer/dart/element/element2.dart'
show Element2, ClassElement2;
import 'package:build/build.dart' show BuildStep;
import 'package:impruvon_builder/impruvon_builder.dart';
import 'package:source_gen/source_gen.dart'
show ConstantReader, GeneratorForAnnotation, InvalidGenerationSourceError;
class IdTypeGenerator extends GeneratorForAnnotation<IdType> {
final Map<String, dynamic> config;
IdTypeGenerator(this.config);
@override
String generateForAnnotatedElement(
Element2 element,
ConstantReader annotation,
BuildStep buildStep,
) {
if (element is! ClassElement2) {
throw InvalidGenerationSourceError(
'@IdType can only be used on classes. "$element" is not a class.',
element: element,
);
}
final typeName =
annotation.peek('name')?.stringValue ?? '${element.displayName}Id';
final code =
'''
extension type const $typeName(int id) implements Object {
factory $typeName.fromJson(int id) => $typeName(id);
int toJson() => id;
static $typeName? tryParse(String? value) {
final id = int.tryParse(value ?? '');
return id == null ? null : $typeName(id);
}
}
''';
return code;
}
}