diff --git a/json_serializable/lib/src/decode_helper.dart b/json_serializable/lib/src/decode_helper.dart index c64c9d1a..305ca937 100644 --- a/json_serializable/lib/src/decode_helper.dart +++ b/json_serializable/lib/src/decode_helper.dart @@ -90,9 +90,10 @@ mixin DecodeHelper implements HelperCore { if (config.checked) { final classLiteral = escapeDartString(element.name!); + final helperPrefix = jsonAnnotationHelperPrefix(element.library); final sectionBuffer = StringBuffer() ..write(''' - \$checkedCreate( + ${helperPrefix}\$checkedCreate( $classLiteral, json, (\$checkedConvert) {\n''') @@ -202,7 +203,8 @@ mixin DecodeHelper implements HelperCore { } if (args.isNotEmpty) { - yield '\$checkKeys(json, ${args.map((e) => '$e, ').join()});\n'; + final helperPrefix = jsonAnnotationHelperPrefix(element.library); + yield '${helperPrefix}\$checkKeys(json, ${args.map((e) => '$e, ').join()});\n'; } } diff --git a/json_serializable/lib/src/type_helpers/enum_helper.dart b/json_serializable/lib/src/type_helpers/enum_helper.dart index ce7641fd..5b49b0b2 100644 --- a/json_serializable/lib/src/type_helpers/enum_helper.dart +++ b/json_serializable/lib/src/type_helpers/enum_helper.dart @@ -9,6 +9,7 @@ import 'package:source_helper/source_helper.dart'; import '../enum_utils.dart'; import '../json_key_utils.dart'; import '../type_helper.dart'; +import '../utils.dart'; final simpleExpression = RegExp('^[a-zA-Z_]+\$'); @@ -62,11 +63,14 @@ class EnumHelper extends TypeHelper { ); } - String functionName; + final helperPrefix = jsonAnnotationHelperPrefix( + context.classElement.library, + ); + final String functionName; if (targetType.isNullableType || defaultProvided) { - functionName = r'$enumDecodeNullable'; + functionName = '${helperPrefix}\$enumDecodeNullable'; } else { - functionName = r'$enumDecode'; + functionName = '${helperPrefix}\$enumDecode'; } context.addMember(memberContent); diff --git a/json_serializable/lib/src/utils.dart b/json_serializable/lib/src/utils.dart index ce679f67..b10bf519 100644 --- a/json_serializable/lib/src/utils.dart +++ b/json_serializable/lib/src/utils.dart @@ -304,3 +304,26 @@ extension ExecutableElementExtension on ExecutableElement { const jsonSerializableChecker = TypeChecker.fromUrl( 'package:json_annotation/src/json_serializable.dart#JsonSerializable', ); + +/// Returns the import prefix used for `package:json_annotation`, including a +/// trailing `.`, or an empty string when the import is unprefixed. +/// +/// Generated references to helpers like `$checkedCreate` and `$enumDecode` must +/// use this prefix so they resolve when the annotation library is imported with +/// a prefix. +String jsonAnnotationHelperPrefix(LibraryElement library) { + for (final fragment in library.fragments) { + for (final import in fragment.libraryImports) { + final uri = import.importedLibrary?.uri; + if (uri == null || + uri.scheme != 'package' || + uri.pathSegments.isEmpty || + uri.pathSegments.first != 'json_annotation') { + continue; + } + final prefix = import.prefix?.name; + return prefix == null ? '' : '$prefix.'; + } + } + return ''; +} diff --git a/json_serializable/test/integration/integration_test.dart b/json_serializable/test/integration/integration_test.dart index f2e8e4c8..9ec59974 100644 --- a/json_serializable/test/integration/integration_test.dart +++ b/json_serializable/test/integration/integration_test.dart @@ -9,6 +9,7 @@ import '../test_utils.dart'; import 'converter_examples.dart'; import 'create_per_field_to_json_example.dart'; import 'field_map_example.dart'; +import 'json_annotation_prefix_example.dart'; import 'json_enum_example.dart'; import 'json_keys_example.dart' as js_keys; import 'json_test_common.dart' show Category, Platform, StatusCode; @@ -492,4 +493,15 @@ void main() { )..remove(r'$schema'); expect(nestedSchemaFromExample, standaloneSchema); }); + + test('json_annotation import prefix helpers', () { + final checked = CheckedPrefixModel.fromJson({'field1': 'a', 'field2': 'b'}); + expect(checked.field1, 'a'); + expect(checked.field2, 'b'); + expect(checked.toJson(), {'field1': 'a', 'field2': 'b'}); + + final enumModel = EnumPrefixModel.fromJson({'gender': 'female'}); + expect(enumModel.gender, PrefixModelGender.female); + expect(enumModel.toJson(), {'gender': 'female'}); + }); } diff --git a/json_serializable/test/integration/json_annotation_prefix_example.dart b/json_serializable/test/integration/json_annotation_prefix_example.dart new file mode 100644 index 00000000..4f89cea9 --- /dev/null +++ b/json_serializable/test/integration/json_annotation_prefix_example.dart @@ -0,0 +1,31 @@ +import 'package:json_annotation/json_annotation.dart' as ja; + +part 'json_annotation_prefix_example.g.dart'; + +@ja.JsonSerializable(checked: true) +class CheckedPrefixModel { + final String field1; + final String field2; + + CheckedPrefixModel({required this.field1, required this.field2}); + + factory CheckedPrefixModel.fromJson(Map json) => + _$CheckedPrefixModelFromJson(json); + + Map toJson() => _$CheckedPrefixModelToJson(this); +} + +@ja.JsonSerializable() +class EnumPrefixModel { + @ja.JsonKey(required: true, disallowNullValue: true) + final PrefixModelGender gender; + + EnumPrefixModel({required this.gender}); + + factory EnumPrefixModel.fromJson(Map json) => + _$EnumPrefixModelFromJson(json); + + Map toJson() => _$EnumPrefixModelToJson(this); +} + +enum PrefixModelGender { male, female, other } diff --git a/json_serializable/test/integration/json_annotation_prefix_example.g.dart b/json_serializable/test/integration/json_annotation_prefix_example.g.dart new file mode 100644 index 00000000..c562efbc --- /dev/null +++ b/json_serializable/test/integration/json_annotation_prefix_example.g.dart @@ -0,0 +1,41 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +// ignore_for_file: lines_longer_than_80_chars, text_direction_code_point_in_literal, inference_failure_on_function_invocation, inference_failure_on_collection_literal + +part of 'json_annotation_prefix_example.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +CheckedPrefixModel _$CheckedPrefixModelFromJson(Map json) => + ja.$checkedCreate('CheckedPrefixModel', json, ($checkedConvert) { + final val = CheckedPrefixModel( + field1: $checkedConvert('field1', (v) => v as String), + field2: $checkedConvert('field2', (v) => v as String), + ); + return val; + }); + +Map _$CheckedPrefixModelToJson(CheckedPrefixModel instance) => + {'field1': instance.field1, 'field2': instance.field2}; + +EnumPrefixModel _$EnumPrefixModelFromJson(Map json) { + ja.$checkKeys( + json, + requiredKeys: const ['gender'], + disallowNullValues: const ['gender'], + ); + return EnumPrefixModel( + gender: ja.$enumDecode(_$PrefixModelGenderEnumMap, json['gender']), + ); +} + +Map _$EnumPrefixModelToJson(EnumPrefixModel instance) => + {'gender': _$PrefixModelGenderEnumMap[instance.gender]!}; + +const _$PrefixModelGenderEnumMap = { + PrefixModelGender.male: 'male', + PrefixModelGender.female: 'female', + PrefixModelGender.other: 'other', +}; diff --git a/json_serializable/test/json_serializable_test.dart b/json_serializable/test/json_serializable_test.dart index cca27c7d..124313d8 100644 --- a/json_serializable/test/json_serializable_test.dart +++ b/json_serializable/test/json_serializable_test.dart @@ -50,6 +50,18 @@ Future main() async { 'UnsupportedClass', }, ); + + final jsonAnnotationPrefixTestReader = + await initializeLibraryReaderForDirectory( + p.join('test', 'src'), + '_json_annotation_prefix_test_input.dart', + ); + + testAnnotatedElements( + jsonAnnotationPrefixTestReader, + JsonSerializableGenerator(), + expectedAnnotatedTests: {'CheckedWithPrefix', 'EnumWithPrefix'}, + ); } const _expectedAnnotatedTests = { diff --git a/json_serializable/test/src/_json_annotation_prefix_test_input.dart b/json_serializable/test/src/_json_annotation_prefix_test_input.dart new file mode 100644 index 00000000..82f695fb --- /dev/null +++ b/json_serializable/test/src/_json_annotation_prefix_test_input.dart @@ -0,0 +1,52 @@ +// Copyright (c) 2026, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// @dart=3.8 + +import 'package:json_annotation/json_annotation.dart' as ja; +import 'package:source_gen_test/annotations.dart'; + +@ShouldGenerate(r''' +CheckedWithPrefix _$CheckedWithPrefixFromJson(Map json) => + ja.$checkedCreate('CheckedWithPrefix', json, ($checkedConvert) { + final val = CheckedWithPrefix( + $checkedConvert('field1', (v) => v as String), + $checkedConvert('field2', (v) => v as String), + ); + return val; + }); +''') +@ja.JsonSerializable(checked: true, createToJson: false) +class CheckedWithPrefix { + final String field1; + final String field2; + + CheckedWithPrefix(this.field1, this.field2); +} + +@ShouldGenerate(r''' +EnumWithPrefix _$EnumWithPrefixFromJson(Map json) { + ja.$checkKeys( + json, + requiredKeys: const ['gender'], + disallowNullValues: const ['gender'], + ); + return EnumWithPrefix(ja.$enumDecode(_$PrefixGenderEnumMap, json['gender'])); +} + +const _$PrefixGenderEnumMap = { + PrefixGender.male: 'male', + PrefixGender.female: 'female', + PrefixGender.other: 'other', +}; +''') +@ja.JsonSerializable(createToJson: false) +class EnumWithPrefix { + @ja.JsonKey(required: true, disallowNullValue: true) + final PrefixGender gender; + + EnumWithPrefix(this.gender); +} + +enum PrefixGender { male, female, other }