Skip to content

SharedPartBuilders seemingly cannot use code generated by other SharedPartBuilders #787

@alexobviously

Description

@alexobviously

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;
  }
}

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions