You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@fory.apache.org by ch...@apache.org on 2026/05/01 13:57:07 UTC
(fory) branch main updated: feat(dart): support nested container field codec for dart (#3641)
This is an automated email from the ASF dual-hosted git repository.
chaokunyang pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/fory.git
The following commit(s) were added to refs/heads/main by this push:
new c0b083977 feat(dart): support nested container field codec for dart (#3641)
c0b083977 is described below
commit c0b083977116a91ad376891b5f7e3e8362c28180
Author: Shawn Yang <sh...@gmail.com>
AuthorDate: Fri May 1 21:57:03 2026 +0800
feat(dart): support nested container field codec for dart (#3641)
## Why?
Dart needs a canonical field type annotation surface that can describe
nested container element/key/value semantics, including fixed-width and
encoded numeric types, without relying on separate numeric wrapper
annotations. The schema fingerprint calculation also needs to include
nested container type IDs consistently across runtimes so
compatible/schema-version checks agree for annotated nested containers.
## What does this PR do?
- Adds Dart nested container field type support through `ForyField(type:
...)`, `MapField`, `ListType`, scalar type specs, and generated support
for nested element/key/value metadata.
- Updates the Dart compiler generator and generated test models to emit
Dart `int` fields with explicit Fory type specs for fixed-width integer
semantics, while keeping exact-width wrapper classes where the Dart
runtime still requires them.
- Removes obsolete Dart numeric wrapper exports/tests for small
fixed-width integer wrappers and adds runtime validation utilities for
integer bounds.
- Extends Dart cross-language and unit coverage for nested annotated
containers, integer round trips, type-spec annotations, signed/unsigned
serializers, and xlang protocol behavior.
- Aligns nested container schema fingerprint generation across Java,
C++, C#, Go, JavaScript, Python, Rust, and Swift, and adds focused
Swift/Java xlang coverage for Dart nested annotated containers.
- Updates Dart guides, package README content, generated-code docs, and
the xlang serialization spec to document the new annotation/type mapping
model.
## Related issues
## AI Contribution Checklist
- [ ] Substantial AI assistance was used in this PR: `yes` / `no`
- [ ] If `yes`, I included a completed [AI Contribution
Checklist](https://github.com/apache/fory/blob/main/AI_POLICY.md#9-contributor-checklist-for-ai-assisted-prs)
in this PR description and the required `AI Usage Disclosure`.
- [ ] If `yes`, my PR description includes the required `ai_review`
summary and screenshot evidence of the final clean AI review results
from both fresh reviewers on the current PR diff or current HEAD after
the latest code changes.
## Does this PR introduce any user-facing change?
- [x] Does this PR introduce any public API change?
- [ ] Does this PR introduce any binary protocol compatibility change?
This changes the Dart annotation/type-spec surface for fixed-width
numeric and nested container field declarations. It updates schema
fingerprint calculation to include nested container metadata
consistently, but does not intentionally change the xlang binary
protocol format.
## Benchmark
---
.github/workflows/ci.yml | 7 +-
README.md | 6 +-
benchmarks/dart/lib/src/models.dart | 69 +-
compiler/fory_compiler/generators/dart.py | 199 +++--
.../fory_compiler/tests/test_dart_generator.py | 38 +-
cpp/fory/serialization/type_resolver.cc | 71 +-
csharp/src/Fory.Generator/ForyObjectGenerator.cs | 101 ++-
dart/README.md | 62 +-
.../fory-test/lib/entity/xlang_test_models.dart | 250 +++---
dart/packages/fory-test/lib/model/person.dart | 7 +-
.../test/compatible_struct_slots_test.dart | 20 +-
.../test/cross_lang_test/xlang_test_main.dart | 36 +-
.../fory-test/test/generated_round_trip_test.dart | 18 +-
dart/packages/fory/README.md | 124 +--
dart/packages/fory/example/example.dart | 5 +-
dart/packages/fory/lib/fory.dart | 7 -
.../fory/lib/src/annotation/fory_field.dart | 77 +-
.../fory/lib/src/annotation/numeric_types.dart | 80 --
.../fory/lib/src/annotation/type_spec.dart | 212 +++--
.../fory/lib/src/codegen/fory_generator.dart | 923 +++++++++++++++------
.../fory/lib/src/codegen/generated_support.dart | 25 +
.../fory/lib/src/context/write_context.dart | 22 +
dart/packages/fory/lib/src/fory.dart | 46 +
.../fory/lib/src/resolver/type_resolver.dart | 59 +-
.../lib/src/serializer/primitive_serializers.dart | 59 +-
.../lib/src/serializer/serializer_support.dart | 34 -
dart/packages/fory/lib/src/types/int16.dart | 145 ----
dart/packages/fory/lib/src/types/int32.dart | 145 ----
dart/packages/fory/lib/src/types/int8.dart | 160 ----
dart/packages/fory/lib/src/types/uint16.dart | 147 ----
dart/packages/fory/lib/src/types/uint32.dart | 147 ----
dart/packages/fory/lib/src/types/uint8.dart | 147 ----
dart/packages/fory/lib/src/util/hash_util.dart | 72 +-
.../packages/fory/lib/src/util/int_validation.dart | 66 ++
dart/packages/fory/test/container_ref_test.dart | 35 +-
.../fory/test/manual_registration_test.dart | 4 +-
dart/packages/fory/test/nested_type_spec_test.dart | 274 ++++++
dart/packages/fory/test/numeric_wrapper_test.dart | 475 -----------
.../fory/test/runtime_validation_test.dart | 4 +-
.../packages/fory/test/signed_serializer_test.dart | 67 +-
.../fory/test/type_spec_annotation_test.dart | 215 ++---
.../fory/test/unsigned_serializer_test.dart | 93 ++-
dart/packages/fory/test/xlang_protocol_test.dart | 21 +-
dart/test/workspace_smoke_test.dart | 6 +-
docs/compiler/generated-code.md | 12 +-
docs/compiler/index.md | 2 +-
docs/compiler/schema-idl.md | 36 +-
docs/guide/dart/basic-serialization.md | 12 +-
docs/guide/dart/code-generation.md | 4 +-
docs/guide/dart/cross-language.md | 20 +-
docs/guide/dart/field-configuration.md | 37 +-
docs/guide/dart/index.md | 11 +-
docs/guide/dart/schema-evolution.md | 2 +-
docs/guide/dart/supported-types.md | 75 +-
docs/guide/dart/troubleshooting.md | 8 +-
docs/guide/dart/web-platform-support.md | 17 +-
docs/guide/xlang/field-reference-tracking.md | 4 +-
docs/specification/xlang_serialization_spec.md | 7 +-
go/fory/field_info.go | 90 +-
go/fory/struct_init.go | 11 +-
.../main/java/org/apache/fory/meta/FieldTypes.java | 6 +-
.../apache/fory/serializer/struct/Fingerprint.java | 84 +-
.../java/org/apache/fory/xlang/DartXlangTest.java | 12 +
javascript/packages/core/lib/meta/TypeMeta.ts | 32 +-
python/pyfory/struct.py | 159 +++-
python/pyfory/tests/test_cross_language.py | 1 +
rust/fory-derive/src/object/util.rs | 204 +++--
swift/Sources/Fory/MacroDeclarations.swift | 3 +
swift/Sources/ForyMacro/ForyObjectMacro.swift | 224 ++++-
swift/Tests/ForyTests/CompatibilityTests.swift | 31 +
70 files changed, 3104 insertions(+), 2780 deletions(-)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 3a5b8db11..5d71d2208 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -1333,7 +1333,8 @@ jobs:
Person();
String name = '';
- Int32 age = Int32(0);
+ @ForyField(type: Int32Type())
+ int age = 0;
}
EOF
cat <<'EOF' | sed 's/^ //' > "$FLUTTER_SMOKE_DIR/lib/main.dart"
@@ -1363,13 +1364,13 @@ jobs:
final person = Person()
..name = 'Ada'
- ..age = Int32(36);
+ ..age = 36;
final bytes = fory.serialize(person);
final roundTrip = fory.deserialize<Person>(bytes);
expect(roundTrip.name, 'Ada');
- expect(roundTrip.age, Int32(36));
+ expect(roundTrip.age, 36);
});
}
EOF
diff --git a/README.md b/README.md
index f22583a90..4ce534801 100644
--- a/README.md
+++ b/README.md
@@ -441,7 +441,9 @@ class Person {
Person();
String name = '';
- Int32 age = Int32(0);
+
+ @ForyField(type: Int32Type())
+ int age = 0;
}
void main() {
@@ -455,7 +457,7 @@ void main() {
final person = Person()
..name = 'chaokunyang'
- ..age = Int32(28);
+ ..age = 28;
final bytes = fory.serialize(person);
final result = fory.deserialize<Person>(bytes);
diff --git a/benchmarks/dart/lib/src/models.dart b/benchmarks/dart/lib/src/models.dart
index a4ea9ff26..77c077ecf 100644
--- a/benchmarks/dart/lib/src/models.dart
+++ b/benchmarks/dart/lib/src/models.dart
@@ -50,29 +50,21 @@ class NumericStruct {
required this.f8,
});
- @ForyField(id: 1)
- @Int32Type()
+ @ForyField(id: 1, type: Int32Type())
final int f1;
- @ForyField(id: 2)
- @Int32Type()
+ @ForyField(id: 2, type: Int32Type())
final int f2;
- @ForyField(id: 3)
- @Int32Type()
+ @ForyField(id: 3, type: Int32Type())
final int f3;
- @ForyField(id: 4)
- @Int32Type()
+ @ForyField(id: 4, type: Int32Type())
final int f4;
- @ForyField(id: 5)
- @Int32Type()
+ @ForyField(id: 5, type: Int32Type())
final int f5;
- @ForyField(id: 6)
- @Int32Type()
+ @ForyField(id: 6, type: Int32Type())
final int f6;
- @ForyField(id: 7)
- @Int32Type()
+ @ForyField(id: 7, type: Int32Type())
final int f7;
- @ForyField(id: 8)
- @Int32Type()
+ @ForyField(id: 8, type: Int32Type())
final int f8;
}
@@ -103,39 +95,31 @@ class Sample {
required this.string,
});
- @ForyField(id: 1)
- @Int32Type()
+ @ForyField(id: 1, type: Int32Type())
final int intValue;
- @ForyField(id: 2)
- @Int64Type()
+ @ForyField(id: 2, type: Int64Type())
final int longValue;
@ForyField(id: 3)
final Float32 floatValue;
@ForyField(id: 4)
final double doubleValue;
- @ForyField(id: 5)
- @Int32Type()
+ @ForyField(id: 5, type: Int32Type())
final int shortValue;
- @ForyField(id: 6)
- @Int32Type()
+ @ForyField(id: 6, type: Int32Type())
final int charValue;
@ForyField(id: 7)
final bool booleanValue;
- @ForyField(id: 8)
- @Int32Type()
+ @ForyField(id: 8, type: Int32Type())
final int intValueBoxed;
- @ForyField(id: 9)
- @Int64Type()
+ @ForyField(id: 9, type: Int64Type())
final int longValueBoxed;
@ForyField(id: 10)
final Float32 floatValueBoxed;
@ForyField(id: 11)
final double doubleValueBoxed;
- @ForyField(id: 12)
- @Int32Type()
+ @ForyField(id: 12, type: Int32Type())
final int shortValueBoxed;
- @ForyField(id: 13)
- @Int32Type()
+ @ForyField(id: 13, type: Int32Type())
final int charValueBoxed;
@ForyField(id: 14)
final bool booleanValueBoxed;
@@ -178,22 +162,17 @@ class Media {
final String uri;
@ForyField(id: 2)
final String title;
- @ForyField(id: 3)
- @Int32Type()
+ @ForyField(id: 3, type: Int32Type())
final int width;
- @ForyField(id: 4)
- @Int32Type()
+ @ForyField(id: 4, type: Int32Type())
final int height;
@ForyField(id: 5)
final String format;
- @ForyField(id: 6)
- @Int64Type()
+ @ForyField(id: 6, type: Int64Type())
final int duration;
- @ForyField(id: 7)
- @Int64Type()
+ @ForyField(id: 7, type: Int64Type())
final int size;
- @ForyField(id: 8)
- @Int32Type()
+ @ForyField(id: 8, type: Int32Type())
final int bitrate;
@ForyField(id: 9)
final bool hasBitrate;
@@ -219,11 +198,9 @@ class Image {
final String uri;
@ForyField(id: 2)
final String title;
- @ForyField(id: 3)
- @Int32Type()
+ @ForyField(id: 3, type: Int32Type())
final int width;
- @ForyField(id: 4)
- @Int32Type()
+ @ForyField(id: 4, type: Int32Type())
final int height;
@ForyField(id: 5)
final MediaSize size;
diff --git a/compiler/fory_compiler/generators/dart.py b/compiler/fory_compiler/generators/dart.py
index e13ea0d5a..bcad160f5 100644
--- a/compiler/fory_compiler/generators/dart.py
+++ b/compiler/fory_compiler/generators/dart.py
@@ -45,22 +45,22 @@ class DartGenerator(BaseGenerator):
PRIMITIVE_MAP = {
PrimitiveKind.BOOL: "bool",
- PrimitiveKind.INT8: "Int8",
- PrimitiveKind.INT16: "Int16",
- PrimitiveKind.INT32: "Int32",
- PrimitiveKind.VARINT32: "Int32",
- PrimitiveKind.INT64: "int",
- PrimitiveKind.VARINT64: "int",
- PrimitiveKind.TAGGED_INT64: "int",
- PrimitiveKind.UINT8: "UInt8",
- PrimitiveKind.UINT16: "UInt16",
- PrimitiveKind.UINT32: "UInt32",
- PrimitiveKind.VAR_UINT32: "UInt32",
- PrimitiveKind.UINT64: "int",
- PrimitiveKind.VAR_UINT64: "int",
- PrimitiveKind.TAGGED_UINT64: "int",
+ PrimitiveKind.INT8: "int",
+ PrimitiveKind.INT16: "int",
+ PrimitiveKind.INT32: "int",
+ PrimitiveKind.VARINT32: "int",
+ PrimitiveKind.INT64: "Int64",
+ PrimitiveKind.VARINT64: "Int64",
+ PrimitiveKind.TAGGED_INT64: "Int64",
+ PrimitiveKind.UINT8: "int",
+ PrimitiveKind.UINT16: "int",
+ PrimitiveKind.UINT32: "int",
+ PrimitiveKind.VAR_UINT32: "int",
+ PrimitiveKind.UINT64: "Uint64",
+ PrimitiveKind.VAR_UINT64: "Uint64",
+ PrimitiveKind.TAGGED_UINT64: "Uint64",
PrimitiveKind.FLOAT16: "Float16",
- PrimitiveKind.BFLOAT16: "Float16",
+ PrimitiveKind.BFLOAT16: "Bfloat16",
PrimitiveKind.FLOAT32: "Float32",
PrimitiveKind.FLOAT64: "double",
PrimitiveKind.STRING: "String",
@@ -506,7 +506,7 @@ class DartGenerator(BaseGenerator):
converted_value = self._conversion_expression(
field_type.value_type,
"mapValue",
- False,
+ field_type.value_optional,
parent_stack,
)
return f"Map<{key_type}, {value_type}>.of((({value_expr} as Map)).map((key, mapValue) => MapEntry({converted_key}, {converted_value})))"
@@ -526,7 +526,7 @@ class DartGenerator(BaseGenerator):
elif isinstance(field_type, MapType):
base = (
f"Map<{self.dart_type(field_type.key_type, parent_stack=parent_stack)}, "
- f"{self.dart_type(field_type.value_type, parent_stack=parent_stack)}>"
+ f"{self.dart_type(field_type.value_type, field_type.value_optional, parent_stack)}>"
)
elif isinstance(field_type, NamedType):
resolved = self.resolve_type(field_type.name, parent_stack)
@@ -568,6 +568,8 @@ class DartGenerator(BaseGenerator):
PrimitiveKind.UINT64: "Uint64List",
PrimitiveKind.VAR_UINT64: "Uint64List",
PrimitiveKind.TAGGED_UINT64: "Uint64List",
+ PrimitiveKind.FLOAT16: "Float16List",
+ PrimitiveKind.BFLOAT16: "Bfloat16List",
PrimitiveKind.FLOAT32: "Float32List",
PrimitiveKind.FLOAT64: "Float64List",
}
@@ -589,22 +591,22 @@ class DartGenerator(BaseGenerator):
if isinstance(t, PrimitiveType):
return {
PrimitiveKind.BOOL: "false",
- PrimitiveKind.INT8: "Int8(0)",
- PrimitiveKind.INT16: "Int16(0)",
- PrimitiveKind.INT32: "Int32(0)",
- PrimitiveKind.VARINT32: "Int32(0)",
- PrimitiveKind.INT64: "0",
- PrimitiveKind.VARINT64: "0",
- PrimitiveKind.TAGGED_INT64: "0",
- PrimitiveKind.UINT8: "UInt8(0)",
- PrimitiveKind.UINT16: "UInt16(0)",
- PrimitiveKind.UINT32: "UInt32(0)",
- PrimitiveKind.VAR_UINT32: "UInt32(0)",
- PrimitiveKind.UINT64: "0",
- PrimitiveKind.VAR_UINT64: "0",
- PrimitiveKind.TAGGED_UINT64: "0",
- PrimitiveKind.FLOAT16: "Float16.zero",
- PrimitiveKind.BFLOAT16: "Float16.zero",
+ PrimitiveKind.INT8: "0",
+ PrimitiveKind.INT16: "0",
+ PrimitiveKind.INT32: "0",
+ PrimitiveKind.VARINT32: "0",
+ PrimitiveKind.INT64: "Int64(0)",
+ PrimitiveKind.VARINT64: "Int64(0)",
+ PrimitiveKind.TAGGED_INT64: "Int64(0)",
+ PrimitiveKind.UINT8: "0",
+ PrimitiveKind.UINT16: "0",
+ PrimitiveKind.UINT32: "0",
+ PrimitiveKind.VAR_UINT32: "0",
+ PrimitiveKind.UINT64: "Uint64(0)",
+ PrimitiveKind.VAR_UINT64: "Uint64(0)",
+ PrimitiveKind.TAGGED_UINT64: "Uint64(0)",
+ PrimitiveKind.FLOAT16: "Float16(0)",
+ PrimitiveKind.BFLOAT16: "Bfloat16(0)",
PrimitiveKind.FLOAT32: "Float32(0)",
PrimitiveKind.FLOAT64: "0.0",
PrimitiveKind.STRING: "''",
@@ -623,7 +625,7 @@ class DartGenerator(BaseGenerator):
)
if isinstance(t, MapType):
key_type = self.dart_type(t.key_type, parent_stack=parent_stack)
- value_type = self.dart_type(t.value_type, parent_stack=parent_stack)
+ value_type = self.dart_type(t.value_type, t.value_optional, parent_stack)
return f"<{key_type}, {value_type}>{{}}"
if isinstance(t, NamedType):
resolved = self.resolve_type(t.name, parent_stack)
@@ -664,38 +666,85 @@ class DartGenerator(BaseGenerator):
field: Field,
parent_stack: Optional[List[Message]] = None,
) -> List[str]:
- annotations: List[str] = []
- numeric_annotation = self.numeric_annotation(field.field_type)
- if numeric_annotation is not None:
- annotations.append(numeric_annotation)
args: List[str] = []
+ type_spec = self.type_spec_expression(
+ field.field_type,
+ parent_stack,
+ element_ref_override=field.element_ref,
+ )
+ if type_spec is not None:
+ args.append(f"type: {type_spec}")
if field.tag_id is not None:
args.append(f"id: {field.tag_id}")
if field.ref:
args.append("ref: true")
if args:
- annotations.append(f"@ForyField({', '.join(args)})")
- # Emit container TypeSpec annotations for nested ref tracking.
- if isinstance(field.field_type, ListType) and (
- field.element_ref or field.field_type.element_ref
- ):
- annotations.append("@ListType(element: ValueType.ref())")
- if isinstance(field.field_type, MapType) and field.field_type.value_ref:
- annotations.append("@MapType(value: ValueType.ref())")
- return annotations
+ return [f"@ForyField({', '.join(args)})"]
+ return []
- def numeric_annotation(self, field_type: FieldType) -> Optional[str]:
- if not isinstance(field_type, PrimitiveType):
+ def type_spec_expression(
+ self,
+ field_type: FieldType,
+ parent_stack: Optional[List[Message]] = None,
+ *,
+ ref_override: bool = False,
+ element_ref_override: bool = False,
+ ) -> Optional[str]:
+ if isinstance(field_type, PrimitiveType):
+ primitive = {
+ PrimitiveKind.INT8: "Int8Type()",
+ PrimitiveKind.INT16: "Int16Type()",
+ PrimitiveKind.INT32: "Int32Type(encoding: Encoding.fixed)",
+ PrimitiveKind.VARINT32: "Int32Type(encoding: Encoding.varint)",
+ PrimitiveKind.INT64: "Int64Type(encoding: Encoding.fixed)",
+ PrimitiveKind.VARINT64: "Int64Type(encoding: Encoding.varint)",
+ PrimitiveKind.TAGGED_INT64: "Int64Type(encoding: Encoding.tagged)",
+ PrimitiveKind.UINT8: "Uint8Type()",
+ PrimitiveKind.UINT16: "Uint16Type()",
+ PrimitiveKind.UINT32: "Uint32Type(encoding: Encoding.fixed)",
+ PrimitiveKind.VAR_UINT32: "Uint32Type(encoding: Encoding.varint)",
+ PrimitiveKind.UINT64: "Uint64Type(encoding: Encoding.fixed)",
+ PrimitiveKind.VAR_UINT64: "Uint64Type(encoding: Encoding.varint)",
+ PrimitiveKind.TAGGED_UINT64: "Uint64Type(encoding: Encoding.tagged)",
+ PrimitiveKind.FLOAT16: "Float16Type()",
+ PrimitiveKind.BFLOAT16: "Bfloat16Type()",
+ PrimitiveKind.FLOAT32: "Float32Type()",
+ }.get(field_type.kind)
+ if primitive is not None:
+ return primitive
+ if ref_override:
+ return "DeclaredType(ref: true)"
+ return None
+ if isinstance(field_type, NamedType):
+ if ref_override:
+ return "DeclaredType(ref: true)"
return None
- return {
- PrimitiveKind.INT32: "@Int32Type(compress: false)",
- PrimitiveKind.INT64: "@Int64Type(encoding: LongEncoding.fixed)",
- PrimitiveKind.TAGGED_INT64: "@Int64Type(encoding: LongEncoding.tagged)",
- PrimitiveKind.VAR_UINT32: "@Uint32Type()",
- PrimitiveKind.UINT64: "@Uint64Type(encoding: LongEncoding.fixed)",
- PrimitiveKind.VAR_UINT64: "@Uint64Type()",
- PrimitiveKind.TAGGED_UINT64: "@Uint64Type(encoding: LongEncoding.tagged)",
- }.get(field_type.kind)
+ if isinstance(field_type, ListType):
+ element_ref = field_type.element_ref or element_ref_override
+ element_spec = self.type_spec_expression(
+ field_type.element_type,
+ parent_stack,
+ ref_override=element_ref,
+ )
+ if element_spec is None:
+ return None
+ return f"ListType(element: {element_spec})"
+ if isinstance(field_type, MapType):
+ key_spec = self.type_spec_expression(field_type.key_type, parent_stack)
+ value_spec = self.type_spec_expression(
+ field_type.value_type,
+ parent_stack,
+ ref_override=field_type.value_ref,
+ )
+ args: List[str] = []
+ if key_spec is not None:
+ args.append(f"key: {key_spec}")
+ if value_spec is not None:
+ args.append(f"value: {value_spec}")
+ if not args:
+ return None
+ return f"MapType({', '.join(args)})"
+ return None
def enum_case_name(self, enum: Enum, value: EnumValue) -> str:
name = value.name
@@ -985,7 +1034,7 @@ class DartGenerator(BaseGenerator):
args.append(
self._field_type_literal_from_type(
field_type.value_type,
- False,
+ field_type.value_optional,
field_type.value_ref,
indent + 2,
parent_stack,
@@ -1030,22 +1079,22 @@ class DartGenerator(BaseGenerator):
if isinstance(field_type, PrimitiveType):
return {
PrimitiveKind.BOOL: ("bool", "TypeIds.boolType"),
- PrimitiveKind.INT8: ("Int8", "TypeIds.int8"),
- PrimitiveKind.INT16: ("Int16", "TypeIds.int16"),
- PrimitiveKind.INT32: ("Int32", "TypeIds.int32"),
- PrimitiveKind.VARINT32: ("Int32", "TypeIds.varInt32"),
- PrimitiveKind.INT64: ("int", "TypeIds.int64"),
- PrimitiveKind.VARINT64: ("int", "TypeIds.varInt64"),
- PrimitiveKind.TAGGED_INT64: ("int", "TypeIds.taggedInt64"),
- PrimitiveKind.UINT8: ("UInt8", "TypeIds.uint8"),
- PrimitiveKind.UINT16: ("UInt16", "TypeIds.uint16"),
- PrimitiveKind.UINT32: ("UInt32", "TypeIds.uint32"),
- PrimitiveKind.VAR_UINT32: ("UInt32", "TypeIds.varUint32"),
- PrimitiveKind.UINT64: ("int", "TypeIds.uint64"),
- PrimitiveKind.VAR_UINT64: ("int", "TypeIds.varUint64"),
- PrimitiveKind.TAGGED_UINT64: ("int", "TypeIds.taggedUint64"),
+ PrimitiveKind.INT8: ("int", "TypeIds.int8"),
+ PrimitiveKind.INT16: ("int", "TypeIds.int16"),
+ PrimitiveKind.INT32: ("int", "TypeIds.int32"),
+ PrimitiveKind.VARINT32: ("int", "TypeIds.varInt32"),
+ PrimitiveKind.INT64: ("Int64", "TypeIds.int64"),
+ PrimitiveKind.VARINT64: ("Int64", "TypeIds.varInt64"),
+ PrimitiveKind.TAGGED_INT64: ("Int64", "TypeIds.taggedInt64"),
+ PrimitiveKind.UINT8: ("int", "TypeIds.uint8"),
+ PrimitiveKind.UINT16: ("int", "TypeIds.uint16"),
+ PrimitiveKind.UINT32: ("int", "TypeIds.uint32"),
+ PrimitiveKind.VAR_UINT32: ("int", "TypeIds.varUint32"),
+ PrimitiveKind.UINT64: ("Uint64", "TypeIds.uint64"),
+ PrimitiveKind.VAR_UINT64: ("Uint64", "TypeIds.varUint64"),
+ PrimitiveKind.TAGGED_UINT64: ("Uint64", "TypeIds.taggedUint64"),
PrimitiveKind.FLOAT16: ("Float16", "TypeIds.float16"),
- PrimitiveKind.BFLOAT16: ("Float16", "TypeIds.float16"),
+ PrimitiveKind.BFLOAT16: ("Bfloat16", "TypeIds.bfloat16"),
PrimitiveKind.FLOAT32: ("Float32", "TypeIds.float32"),
PrimitiveKind.FLOAT64: ("double", "TypeIds.float64"),
PrimitiveKind.STRING: ("String", "TypeIds.string"),
@@ -1070,6 +1119,8 @@ class DartGenerator(BaseGenerator):
"Uint16List": "TypeIds.uint16Array",
"Uint32List": "TypeIds.uint32Array",
"Uint64List": "TypeIds.uint64Array",
+ "Float16List": "TypeIds.float16Array",
+ "Bfloat16List": "TypeIds.bfloat16Array",
"Float32List": "TypeIds.float32Array",
"Float64List": "TypeIds.float64Array",
}[arr]
diff --git a/compiler/fory_compiler/tests/test_dart_generator.py b/compiler/fory_compiler/tests/test_dart_generator.py
index 3311adb98..5f7e3e285 100644
--- a/compiler/fory_compiler/tests/test_dart_generator.py
+++ b/compiler/fory_compiler/tests/test_dart_generator.py
@@ -56,9 +56,17 @@ def test_dart_generator_emits_annotated_structs_and_generated_part_registration(
assert "part 'demo.fory.dart';" in file.content
assert "@ForyStruct()" in file.content
assert "final class Scalar {" in file.content
- assert "@Int32Type(compress: false)" in file.content
- assert "@Uint64Type(encoding: LongEncoding.tagged)" in file.content
- assert "@ForyField(id: 1)" in file.content
+ assert (
+ "@ForyField(type: Int32Type(encoding: Encoding.fixed), id: 1)" in file.content
+ )
+ assert (
+ "@ForyField(type: Int32Type(encoding: Encoding.varint), id: 2)" in file.content
+ )
+ assert (
+ "@ForyField(type: Uint64Type(encoding: Encoding.tagged), id: 3)" in file.content
+ )
+ assert "int fixedValue = 0;" in file.content
+ assert "Uint64 taggedValue = Uint64(0);" in file.content
assert (
"DemoFory.register(fory, Scalar, id: registrationMode.id, namespace: registrationMode.namespace, typeName: registrationMode.typeName);"
in file.content
@@ -138,7 +146,11 @@ def test_dart_generator_uses_typed_lists_for_non_nullable_primitive_lists():
)
assert "Int32List ints = Int32List(0);" in file.content
- assert "List<Int32?> nullableInts = <Int32?>[];" in file.content
+ assert (
+ "@ForyField(type: ListType(element: Int32Type(encoding: Encoding.varint)), id: 2)"
+ in file.content
+ )
+ assert "List<int?> nullableInts = <int?>[];" in file.content
assert "Int32List? maybeInts = null;" in file.content
assert "factory ValueUnion.values(Uint32List value)" in file.content
@@ -176,10 +188,14 @@ def test_dart_generator_emits_container_ref_annotations_for_builder_metadata():
"""
)
- assert "@ForyField(id: 1)" in file.content
- assert "@ListType(element: ValueType.ref())" in file.content
- assert "@ForyField(id: 2)" in file.content
- assert "@MapType(value: ValueType.ref())" in file.content
+ assert (
+ "@ForyField(type: ListType(element: DeclaredType(ref: true)), id: 1)"
+ in file.content
+ )
+ assert (
+ "@ForyField(type: MapType(value: DeclaredType(ref: true)), id: 2)"
+ in file.content
+ )
assert "@ForyField(id: 3, ref: true)" in file.content
@@ -194,8 +210,10 @@ def test_dart_generator_marks_map_value_ref_messages_as_ref_capable():
"""
)
- assert "@ForyField(id: 1)" in file.content
- assert "@MapType(value: ValueType.ref())" in file.content
+ assert (
+ "@ForyField(type: MapType(value: DeclaredType(ref: true)), id: 1)"
+ in file.content
+ )
assert "Map<String, Node> byName = <String, Node>{};" in file.content
diff --git a/cpp/fory/serialization/type_resolver.cc b/cpp/fory/serialization/type_resolver.cc
index 563afb6b6..fe1faca19 100644
--- a/cpp/fory/serialization/type_resolver.cc
+++ b/cpp/fory/serialization/type_resolver.cc
@@ -24,6 +24,7 @@
#include "fory/type/type.h"
#include <algorithm>
#include <cstring>
+#include <functional>
#include <map>
#include <unordered_map>
@@ -1123,10 +1124,8 @@ std::string TypeMeta::compute_struct_fingerprint(
// Field Components:
// - field_id_or_name: tag ID as string if configured, otherwise snake_case
// field name
- // - type_id: Fory TypeId as decimal string (e.g., "4" for INT32)
- // - ref: "1" if reference tracking enabled, "0" otherwise (always "0" in
- // C++)
- // - nullable: "1" if null flag is written, "0" otherwise
+ // - field_type_fingerprint:
+ // <type_id>,<ref>,<nullable>[<nested_type_fingerprint>]
//
// Example fingerprints:
// - With tag IDs: "0,4,0,0;1,12,0,1;"
@@ -1149,30 +1148,58 @@ std::string TypeMeta::compute_struct_fingerprint(
// reserve a rough estimate to avoid reallocations
fingerprint.reserve(sorted_fields.size() * 24);
+ auto fingerprint_type_id = [](uint32_t type_id) -> uint32_t {
+ if (type_id == static_cast<uint32_t>(TypeId::ENUM) ||
+ type_id == static_cast<uint32_t>(TypeId::NAMED_ENUM) ||
+ type_id == static_cast<uint32_t>(TypeId::STRUCT) ||
+ type_id == static_cast<uint32_t>(TypeId::COMPATIBLE_STRUCT) ||
+ type_id == static_cast<uint32_t>(TypeId::NAMED_STRUCT) ||
+ type_id == static_cast<uint32_t>(TypeId::NAMED_COMPATIBLE_STRUCT) ||
+ type_id == static_cast<uint32_t>(TypeId::EXT) ||
+ type_id == static_cast<uint32_t>(TypeId::NAMED_EXT) ||
+ type_id == static_cast<uint32_t>(TypeId::UNION) ||
+ type_id == static_cast<uint32_t>(TypeId::TYPED_UNION) ||
+ type_id == static_cast<uint32_t>(TypeId::NAMED_UNION)) {
+ return static_cast<uint32_t>(TypeId::UNKNOWN);
+ }
+ return type_id;
+ };
+ std::function<void(std::string &, const FieldType &, bool, bool)>
+ append_field_type = [&](std::string &out, const FieldType &field_type,
+ bool include_ref, bool include_nullable) {
+ out.append(std::to_string(fingerprint_type_id(field_type.type_id)));
+ out.push_back(',');
+ out.push_back(include_ref && field_type.track_ref ? '1' : '0');
+ out.push_back(',');
+ out.push_back(include_nullable && field_type.nullable ? '1' : '0');
+ if (field_type.type_id == static_cast<uint32_t>(TypeId::LIST) ||
+ field_type.type_id == static_cast<uint32_t>(TypeId::SET)) {
+ out.push_back('[');
+ if (!field_type.generics.empty()) {
+ append_field_type(out, field_type.generics.front(), false, false);
+ }
+ out.push_back(']');
+ } else if (field_type.type_id == static_cast<uint32_t>(TypeId::MAP)) {
+ out.push_back('[');
+ if (!field_type.generics.empty()) {
+ append_field_type(out, field_type.generics.front(), false, false);
+ }
+ out.push_back('|');
+ if (field_type.generics.size() > 1) {
+ append_field_type(out, field_type.generics[1], false, false);
+ }
+ out.push_back(']');
+ }
+ };
+
for (const auto &fi : sorted_fields) {
std::string field_id_or_name = fi.field_id >= 0
? std::to_string(fi.field_id)
: normalize_field_name(fi.field_name);
fingerprint.append(field_id_or_name);
fingerprint.push_back(',');
-
- // Java's ObjectSerializer.get_type_id returns Types.UNKNOWN (0) for:
- // - abstract classes, interfaces, and enum types
- // - user-defined struct types (in xlang mode)
- // This aligns the hash computation with Java's behavior.
- uint32_t effective_type_id = fi.field_type.type_id;
- if (effective_type_id == static_cast<uint32_t>(TypeId::ENUM) ||
- effective_type_id == static_cast<uint32_t>(TypeId::NAMED_ENUM) ||
- effective_type_id == static_cast<uint32_t>(TypeId::STRUCT) ||
- effective_type_id == static_cast<uint32_t>(TypeId::NAMED_STRUCT) ||
- effective_type_id == static_cast<uint32_t>(TypeId::UNION)) {
- effective_type_id = static_cast<uint32_t>(TypeId::UNKNOWN);
- }
- fingerprint.append(std::to_string(effective_type_id));
- fingerprint.push_back(',');
- fingerprint.push_back(fi.field_type.track_ref ? '1' : '0');
- fingerprint.push_back(',');
- fingerprint.append(fi.field_type.nullable ? "1;" : "0;");
+ append_field_type(fingerprint, fi.field_type, true, true);
+ fingerprint.push_back(';');
}
return fingerprint;
diff --git a/csharp/src/Fory.Generator/ForyObjectGenerator.cs b/csharp/src/Fory.Generator/ForyObjectGenerator.cs
index c5dc09d5c..c38ac8c91 100644
--- a/csharp/src/Fory.Generator/ForyObjectGenerator.cs
+++ b/csharp/src/Fory.Generator/ForyObjectGenerator.cs
@@ -16,6 +16,7 @@
// under the License.
using System.Collections.Immutable;
+using System.Globalization;
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
@@ -1660,18 +1661,8 @@ public sealed class ForyObjectGenerator : IIncrementalGenerator
bool first = true;
foreach (MemberModel member in ordered)
{
- uint fingerprintTypeId = (member.Classification.IsPrimitive || member.Classification.IsBuiltIn)
- ? member.Classification.TypeId
- : 0;
- string trackRefExpr = member.DynamicAnyKind switch
- {
- DynamicAnyKind.AnyValue => "(trackRef ? 1 : 0)",
- _ => member.Classification.IsBuiltIn || !member.IsRefType
- ? "0"
- : "(trackRef ? 1 : 0)",
- };
- string nullable = member.IsNullable ? "1" : "0";
- string piece = $"\"{EscapeString(member.FieldIdentifier)},{fingerprintTypeId},\" + {trackRefExpr} + \",{nullable};\"";
+ string piece =
+ $"\"{EscapeString(member.FieldIdentifier)},\" + {BuildSchemaFieldTypeFingerprintExpression(member.TypeMeta, "trackRef", includeNullable: true)} + \";\"";
if (!first)
{
sb.Append(" + ");
@@ -1684,6 +1675,92 @@ public sealed class ForyObjectGenerator : IIncrementalGenerator
return sb.ToString().Replace("b_float16", "bfloat16");
}
+ private static string BuildSchemaFieldTypeFingerprintExpression(
+ TypeMetaFieldTypeModel model,
+ string trackRefExpr,
+ bool includeNullable)
+ {
+ string localTrackRefExpr = model.TrackRefByContext
+ ? $"({trackRefExpr} ? 1 : 0)"
+ : "0";
+ string prefix =
+ $"\"{NormalizeSchemaFingerprintTypeId(model.TypeIdExpr).ToString(CultureInfo.InvariantCulture)},\" + {localTrackRefExpr} + \","
+ + (includeNullable && model.Nullable ? "1" : "0")
+ + "\"";
+ if (model.Generics.Length == 0)
+ {
+ return prefix;
+ }
+
+ if (model.Generics.Length == 1)
+ {
+ string child = BuildSchemaFieldTypeFingerprintExpression(model.Generics[0], "false", includeNullable: false);
+ return $"{prefix} + \"[\" + {child} + \"]\"";
+ }
+
+ if (model.Generics.Length == 2)
+ {
+ string key = BuildSchemaFieldTypeFingerprintExpression(model.Generics[0], "false", includeNullable: false);
+ string value = BuildSchemaFieldTypeFingerprintExpression(model.Generics[1], "false", includeNullable: false);
+ return $"{prefix} + \"[\" + {key} + \"|\" + {value} + \"]\"";
+ }
+
+ throw new InvalidOperationException("schema fingerprint supports only list/set/map generic arity");
+ }
+
+ private static uint NormalizeSchemaFingerprintTypeId(string typeIdExpr)
+ {
+ if (!TryParseSchemaFingerprintTypeId(typeIdExpr, out uint typeId))
+ {
+ throw new InvalidOperationException($"unsupported schema fingerprint type id expression {typeIdExpr}");
+ }
+
+ return typeId switch
+ {
+ 0 or 25 or 26 or 27 or 28 or 29 or 30 or 31 or 32 or 33 or 34 or 35 => 0,
+ _ => typeId,
+ };
+ }
+
+ private static bool TryParseSchemaFingerprintTypeId(string typeIdExpr, out uint typeId)
+ {
+ string normalized = typeIdExpr.Replace(" ", string.Empty);
+ if (normalized.StartsWith("(uint)", StringComparison.Ordinal))
+ {
+ normalized = normalized.Substring(6);
+ }
+
+ if (uint.TryParse(normalized, NumberStyles.None, CultureInfo.InvariantCulture, out typeId))
+ {
+ return true;
+ }
+
+ switch (normalized)
+ {
+ case "global::Apache.Fory.TypeId.Unknown":
+ typeId = 0;
+ return true;
+ case "global::Apache.Fory.TypeId.List":
+ typeId = 22;
+ return true;
+ case "global::Apache.Fory.TypeId.Set":
+ typeId = 23;
+ return true;
+ case "global::Apache.Fory.TypeId.Map":
+ typeId = 24;
+ return true;
+ case "global::Apache.Fory.TypeId.Enum":
+ typeId = 25;
+ return true;
+ case "global::Apache.Fory.TypeId.Union":
+ typeId = 33;
+ return true;
+ default:
+ typeId = 0;
+ return false;
+ }
+ }
+
private static string BuildTypeMetaExpression(TypeMetaFieldTypeModel model, string trackRefExpr)
{
string localTrackRefExpr = model.TrackRefByContext ? trackRefExpr : "false";
diff --git a/dart/README.md b/dart/README.md
index e7496e463..dae19b840 100644
--- a/dart/README.md
+++ b/dart/README.md
@@ -21,32 +21,36 @@ including getting started, API reference, and code examples.
## Type Mapping
-| Fory xlang type | Dart type |
-| --------------- | ------------------------ |
-| bool | `bool` |
-| int8 | `fory.Int8` (wrapper) |
-| int16 | `fory.Int16` (wrapper) |
-| int32 | `fory.Int32` (wrapper) |
-| int64 | `int` |
-| float16 | `fory.Float16` (wrapper) |
-| float32 | `fory.Float32` (wrapper) |
-| float64 | `double` |
-| string | `String` |
-| binary | `Uint8List` |
-| local_date | `LocalDate` |
-| timestamp | `Timestamp` |
-| list | `List` |
-| set | `Set` |
-| map | `Map` |
-| enum | `enum` |
-| named_struct | `class` |
-| bool_array | `List<bool>` |
-| int8_array | `Int8List` |
-| int16_array | `Int16List` |
-| int32_array | `Int32List` |
-| int64_array | `Int64List` |
-| float32_array | `Float32List` |
-| float64_array | `Float64List` |
+| Fory xlang type | Dart type |
+| --------------- | ---------------------------------------- |
+| bool | `bool` |
+| int8 | `int` + `@ForyField(type: Int8Type())` |
+| int16 | `int` + `@ForyField(type: Int16Type())` |
+| int32 | `int` + `@ForyField(type: Int32Type())` |
+| int64 | `int` or `Int64` |
+| uint8 | `int` + `@ForyField(type: Uint8Type())` |
+| uint16 | `int` + `@ForyField(type: Uint16Type())` |
+| uint32 | `int` + `@ForyField(type: Uint32Type())` |
+| uint64 | `int` or `Uint64` |
+| float16 | `fory.Float16` (wrapper) |
+| float32 | `fory.Float32` (wrapper) |
+| float64 | `double` |
+| string | `String` |
+| binary | `Uint8List` |
+| local_date | `LocalDate` |
+| timestamp | `Timestamp` |
+| list | `List` |
+| set | `Set` |
+| map | `Map` |
+| enum | `enum` |
+| named_struct | `class` |
+| bool_array | `List<bool>` |
+| int8_array | `Int8List` |
+| int16_array | `Int16List` |
+| int32_array | `Int32List` |
+| int64_array | `Int64List` |
+| float32_array | `Float32List` |
+| float64_array | `Float64List` |
## Quick Start
@@ -62,7 +66,9 @@ class Person {
Person();
String name = '';
- Int32 age = Int32(0);
+
+ @ForyField(type: Int32Type())
+ int age = 0;
}
```
@@ -76,7 +82,7 @@ Serialize and deserialize:
final fory = Fory();
PersonFory.register(fory, Person, namespace: 'example', typeName: 'Person');
-final bytes = fory.serialize(Person()..name = 'Ada'..age = Int32(36));
+final bytes = fory.serialize(Person()..name = 'Ada'..age = 36);
final roundTrip = fory.deserialize<Person>(bytes);
```
diff --git a/dart/packages/fory-test/lib/entity/xlang_test_models.dart b/dart/packages/fory-test/lib/entity/xlang_test_models.dart
index 8c6783376..63643d183 100644
--- a/dart/packages/fory-test/lib/entity/xlang_test_models.dart
+++ b/dart/packages/fory-test/lib/entity/xlang_test_models.dart
@@ -84,15 +84,24 @@ class Item {
class SimpleStruct {
SimpleStruct();
- Map<Int32?, double?> f1 = <Int32?, double?>{};
- Int32 f2 = Int32(0);
+ @MapField(key: Int32Type())
+ Map<int?, double?> f1 = <int?, double?>{};
+
+ @ForyField(type: Int32Type())
+ int f2 = 0;
Item f3 = Item();
String f4 = '';
Color f5 = Color.green;
- List<String> f6 = <String>[];
- Int32 f7 = Int32(0);
- Int32 f8 = Int32(0);
- Int32 last = Int32(0);
+ List<String?> f6 = <String?>[];
+
+ @ForyField(type: Int32Type())
+ int f7 = 0;
+
+ @ForyField(type: Int32Type())
+ int f8 = 0;
+
+ @ForyField(type: Int32Type())
+ int last = 0;
}
@ForyStruct()
@@ -113,12 +122,23 @@ class FixedOverrideStruct {
class Item1 {
Item1();
- Int32 f1 = Int32(0);
- Int32 f2 = Int32(0);
- Int32 f3 = Int32(0);
- Int32 f4 = Int32(0);
- Int32 f5 = Int32(0);
- Int32 f6 = Int32(0);
+ @ForyField(type: Int32Type())
+ int f1 = 0;
+
+ @ForyField(type: Int32Type())
+ int f2 = 0;
+
+ @ForyField(type: Int32Type())
+ int f3 = 0;
+
+ @ForyField(type: Int32Type())
+ int f4 = 0;
+
+ @ForyField(type: Int32Type())
+ int f5 = 0;
+
+ @ForyField(type: Int32Type())
+ int f6 = 0;
}
final class Union2 {
@@ -167,11 +187,39 @@ class StructWithMap {
Map<String?, String?> data = <String?, String?>{};
}
+@ForyStruct()
+class NestedAnnotatedContainerSchemaConsistent {
+ NestedAnnotatedContainerSchemaConsistent();
+
+ @MapField(
+ key: Uint32Type(nullable: true, encoding: Encoding.fixed),
+ value: ListType(
+ nullable: true,
+ element: Uint64Type(nullable: true, encoding: Encoding.tagged),
+ ),
+ )
+ Map<int?, List<int?>?> values = <int?, List<int?>?>{};
+}
+
+@ForyStruct()
+class NestedAnnotatedContainerCompatible {
+ NestedAnnotatedContainerCompatible();
+
+ @MapField(
+ key: Uint32Type(nullable: true, encoding: Encoding.fixed),
+ value: ListType(
+ nullable: true,
+ element: Uint64Type(nullable: true, encoding: Encoding.tagged),
+ ),
+ )
+ Map<int?, List<int?>?> values = <int?, List<int?>?>{};
+}
+
@ForyStruct()
class MyStruct {
MyStruct();
- @Int32Type()
+ @ForyField(type: Int32Type())
int id = 0;
}
@@ -206,7 +254,7 @@ class EmptyWrapper {
class VersionCheckStruct {
VersionCheckStruct();
- @Int32Type()
+ @ForyField(type: Int32Type())
int f1 = 0;
@ForyField(nullable: true)
@@ -224,7 +272,7 @@ class Dog implements Animal {
Dog();
@override
- @Int32Type()
+ @ForyField(type: Int32Type())
int age = 0;
@ForyField(nullable: true)
@@ -235,11 +283,11 @@ class Dog implements Animal {
class Cat implements Animal {
Cat();
- @Int32Type()
+ @ForyField(type: Int32Type())
@override
int age = 0;
- @Int32Type()
+ @ForyField(type: Int32Type())
int lives = 0;
}
@@ -247,14 +295,14 @@ class Cat implements Animal {
class AnimalListHolder {
AnimalListHolder();
- List<Animal> animals = <Animal>[];
+ List<Animal?> animals = <Animal?>[];
}
@ForyStruct()
class AnimalMapHolder {
AnimalMapHolder();
- Map<String, Animal> animalMap = <String, Animal>{};
+ Map<String?, Animal?> animalMap = <String?, Animal?>{};
}
@ForyStruct()
@@ -307,20 +355,25 @@ class TwoEnumFieldStruct {
class NullableComprehensiveSchemaConsistent {
NullableComprehensiveSchemaConsistent();
- Int8 byteField = Int8(0);
- Int16 shortField = Int16(0);
- Int32 intField = Int32(0);
+ @ForyField(type: Int8Type())
+ int byteField = 0;
+
+ @ForyField(type: Int16Type())
+ int shortField = 0;
+
+ @ForyField(type: Int32Type())
+ int intField = 0;
int longField = 0;
Float32 floatField = Float32(0);
double doubleField = 0;
bool boolField = false;
String stringField = '';
- List<String> listField = <String>[];
- Set<String> setField = <String>{};
- Map<String, String> mapField = <String, String>{};
+ List<String?> listField = <String?>[];
+ Set<String?> setField = <String?>{};
+ Map<String?, String?> mapField = <String?, String?>{};
- @ForyField(nullable: true)
- Int32? nullableInt;
+ @ForyField(nullable: true, type: Int32Type())
+ int? nullableInt;
@ForyField(nullable: true)
int? nullableLong;
@@ -338,54 +391,61 @@ class NullableComprehensiveSchemaConsistent {
String? nullableString;
@ForyField(nullable: true)
- List<String>? nullableList;
+ List<String?>? nullableList;
@ForyField(nullable: true)
- Set<String>? nullableSet;
+ Set<String?>? nullableSet;
@ForyField(nullable: true)
- Map<String, String>? nullableMap;
+ Map<String?, String?>? nullableMap;
}
@ForyStruct()
class NullableComprehensiveCompatible {
NullableComprehensiveCompatible();
- Int8 byteField = Int8(0);
- Int16 shortField = Int16(0);
- Int32 intField = Int32(0);
+ @ForyField(type: Int8Type())
+ int byteField = 0;
+
+ @ForyField(type: Int16Type())
+ int shortField = 0;
+
+ @ForyField(type: Int32Type())
+ int intField = 0;
int longField = 0;
Float32 floatField = Float32(0);
double doubleField = 0;
bool boolField = false;
- Int32 boxedInt = Int32(0);
+ @ForyField(type: Int32Type())
+ int boxedInt = 0;
int boxedLong = 0;
Float32 boxedFloat = Float32(0);
double boxedDouble = 0;
bool boxedBool = false;
String stringField = '';
- List<String> listField = <String>[];
- Set<String> setField = <String>{};
- Map<String, String> mapField = <String, String>{};
+ List<String?> listField = <String?>[];
+ Set<String?> setField = <String?>{};
+ Map<String?, String?> mapField = <String?, String?>{};
- Int32 nullableInt1 = Int32(0);
+ @ForyField(type: Int32Type())
+ int nullableInt1 = 0;
int nullableLong1 = 0;
Float32 nullableFloat1 = Float32(0);
double nullableDouble1 = 0;
bool nullableBool1 = false;
String nullableString2 = '';
- List<String> nullableList2 = <String>[];
- Set<String> nullableSet2 = <String>{};
- Map<String, String> nullableMap2 = <String, String>{};
+ List<String?> nullableList2 = <String?>[];
+ Set<String?> nullableSet2 = <String?>{};
+ Map<String?, String?> nullableMap2 = <String?, String?>{};
}
@ForyStruct()
class RefInnerSchemaConsistent {
RefInnerSchemaConsistent();
- @Int32Type()
+ @ForyField(type: Int32Type())
int id = 0;
String name = '';
}
@@ -405,7 +465,7 @@ class RefOuterSchemaConsistent {
class RefInnerCompatible {
RefInnerCompatible();
- @Int32Type()
+ @ForyField(type: Int32Type())
int id = 0;
String name = '';
}
@@ -425,7 +485,8 @@ class RefOuterCompatible {
class RefOverrideElement {
RefOverrideElement();
- Int32 id = Int32(0);
+ @ForyField(type: Int32Type())
+ int id = 0;
String name = '';
}
@@ -451,48 +512,46 @@ class CircularRefStruct {
class UnsignedSchemaConsistent {
UnsignedSchemaConsistent();
- Uint8 u8Field = Uint8(0);
- Uint16 u16Field = Uint16(0);
+ @ForyField(type: Uint8Type())
+ int u8Field = 0;
- @Uint32Type(compress: true)
- Uint32 u32VarField = Uint32(0);
+ @ForyField(type: Uint16Type())
+ int u16Field = 0;
- @Uint32Type(compress: false)
- Uint32 u32FixedField = Uint32(0);
+ @ForyField(type: Uint32Type(encoding: Encoding.varint))
+ int u32VarField = 0;
- @Uint64Type(encoding: LongEncoding.varint)
+ @ForyField(type: Uint32Type(encoding: Encoding.fixed))
+ int u32FixedField = 0;
+
+ @ForyField(type: Uint64Type(encoding: Encoding.varint))
int u64VarField = 0;
- @Uint64Type(encoding: LongEncoding.fixed)
+ @ForyField(type: Uint64Type(encoding: Encoding.fixed))
int u64FixedField = 0;
- @Uint64Type(encoding: LongEncoding.tagged)
+ @ForyField(type: Uint64Type(encoding: Encoding.tagged))
int u64TaggedField = 0;
- @ForyField(nullable: true)
- Uint8? u8NullableField;
+ @ForyField(nullable: true, type: Uint8Type())
+ int? u8NullableField;
- @ForyField(nullable: true)
- Uint16? u16NullableField;
+ @ForyField(nullable: true, type: Uint16Type())
+ int? u16NullableField;
- @ForyField(nullable: true)
- @Uint32Type(compress: true)
- Uint32? u32VarNullableField;
+ @ForyField(nullable: true, type: Uint32Type(encoding: Encoding.varint))
+ int? u32VarNullableField;
- @ForyField(nullable: true)
- @Uint32Type(compress: false)
- Uint32? u32FixedNullableField;
+ @ForyField(nullable: true, type: Uint32Type(encoding: Encoding.fixed))
+ int? u32FixedNullableField;
- @ForyField(nullable: true)
- @Uint64Type(encoding: LongEncoding.varint)
+ @ForyField(nullable: true, type: Uint64Type(encoding: Encoding.varint))
int? u64VarNullableField;
- @ForyField(nullable: true)
- @Uint64Type(encoding: LongEncoding.fixed)
+ @ForyField(nullable: true, type: Uint64Type(encoding: Encoding.fixed))
int? u64FixedNullableField;
- @ForyField(nullable: true)
- @Uint64Type(encoding: LongEncoding.tagged)
+ @ForyField(nullable: true, type: Uint64Type(encoding: Encoding.tagged))
int? u64TaggedNullableField;
}
@@ -500,11 +559,10 @@ class UnsignedSchemaConsistent {
class UnsignedSchemaConsistentSimple {
UnsignedSchemaConsistentSimple();
- @Uint64Type(encoding: LongEncoding.tagged)
+ @ForyField(type: Uint64Type(encoding: Encoding.tagged))
int u64Tagged = 0;
- @ForyField(nullable: true)
- @Uint64Type(encoding: LongEncoding.tagged)
+ @ForyField(nullable: true, type: Uint64Type(encoding: Encoding.tagged))
int? u64TaggedNullable;
}
@@ -512,47 +570,45 @@ class UnsignedSchemaConsistentSimple {
class UnsignedSchemaCompatible {
UnsignedSchemaCompatible();
- @ForyField(nullable: true)
- Uint8? u8Field1;
+ @ForyField(nullable: true, type: Uint8Type())
+ int? u8Field1;
- @ForyField(nullable: true)
- Uint16? u16Field1;
+ @ForyField(nullable: true, type: Uint16Type())
+ int? u16Field1;
- @ForyField(nullable: true)
- @Uint32Type(compress: true)
- Uint32? u32VarField1;
+ @ForyField(nullable: true, type: Uint32Type(encoding: Encoding.varint))
+ int? u32VarField1;
- @ForyField(nullable: true)
- @Uint32Type(compress: false)
- Uint32? u32FixedField1;
+ @ForyField(nullable: true, type: Uint32Type(encoding: Encoding.fixed))
+ int? u32FixedField1;
- @ForyField(nullable: true)
- @Uint64Type(encoding: LongEncoding.varint)
+ @ForyField(nullable: true, type: Uint64Type(encoding: Encoding.varint))
int? u64VarField1;
- @ForyField(nullable: true)
- @Uint64Type(encoding: LongEncoding.fixed)
+ @ForyField(nullable: true, type: Uint64Type(encoding: Encoding.fixed))
int? u64FixedField1;
- @ForyField(nullable: true)
- @Uint64Type(encoding: LongEncoding.tagged)
+ @ForyField(nullable: true, type: Uint64Type(encoding: Encoding.tagged))
int? u64TaggedField1;
- Uint8 u8Field2 = Uint8(0);
- Uint16 u16Field2 = Uint16(0);
+ @ForyField(type: Uint8Type())
+ int u8Field2 = 0;
+
+ @ForyField(type: Uint16Type())
+ int u16Field2 = 0;
- @Uint32Type(compress: true)
- Uint32 u32VarField2 = Uint32(0);
+ @ForyField(type: Uint32Type(encoding: Encoding.varint))
+ int u32VarField2 = 0;
- @Uint32Type(compress: false)
- Uint32 u32FixedField2 = Uint32(0);
+ @ForyField(type: Uint32Type(encoding: Encoding.fixed))
+ int u32FixedField2 = 0;
- @Uint64Type(encoding: LongEncoding.varint)
+ @ForyField(type: Uint64Type(encoding: Encoding.varint))
int u64VarField2 = 0;
- @Uint64Type(encoding: LongEncoding.fixed)
+ @ForyField(type: Uint64Type(encoding: Encoding.fixed))
int u64FixedField2 = 0;
- @Uint64Type(encoding: LongEncoding.tagged)
+ @ForyField(type: Uint64Type(encoding: Encoding.tagged))
int u64TaggedField2 = 0;
}
diff --git a/dart/packages/fory-test/lib/model/person.dart b/dart/packages/fory-test/lib/model/person.dart
index a6946637e..7b9ccda23 100644
--- a/dart/packages/fory-test/lib/model/person.dart
+++ b/dart/packages/fory-test/lib/model/person.dart
@@ -33,10 +33,13 @@ class Person {
Person();
String name = '';
- Int32 age = Int32(0);
+ @ForyField(type: Int32Type())
+ int age = 0;
Color favoriteColor = Color.red;
List<String?> tags = <String?>[];
- Map<String, Int32> scores = <String, Int32>{};
+
+ @MapField(value: Int32Type())
+ Map<String, int> scores = <String, int>{};
}
@ForyStruct()
diff --git a/dart/packages/fory-test/test/compatible_struct_slots_test.dart b/dart/packages/fory-test/test/compatible_struct_slots_test.dart
index 7a5ede723..bf2f3a216 100644
--- a/dart/packages/fory-test/test/compatible_struct_slots_test.dart
+++ b/dart/packages/fory-test/test/compatible_struct_slots_test.dart
@@ -44,13 +44,13 @@ void main() {
);
final original = SimpleStruct()
- ..f2 = Int32(1)
- ..f7 = Int32(2)
- ..f8 = Int32(3)
- ..last = Int32(4)
+ ..f2 = (1)
+ ..f7 = (2)
+ ..f8 = (3)
+ ..last = (4)
..f4 = 'outer'
..f6 = <String>['a', 'b']
- ..f1 = <Int32?, double?>{Int32(7): 9.5}
+ ..f1 = <int?, double?>{(7): 9.5}
..f3 = (Item()..name = 'inner')
..f5 = Color.blue;
@@ -62,13 +62,13 @@ void main() {
final secondBytes = fory.serialize(decoded);
final roundTrip = fory.deserialize<SimpleStruct>(secondBytes);
- expect(roundTrip.f2, equals(Int32(1)));
- expect(roundTrip.f7, equals(Int32(2)));
- expect(roundTrip.f8, equals(Int32(3)));
- expect(roundTrip.last, equals(Int32(4)));
+ expect(roundTrip.f2, equals((1)));
+ expect(roundTrip.f7, equals((2)));
+ expect(roundTrip.f8, equals((3)));
+ expect(roundTrip.last, equals((4)));
expect(roundTrip.f4, equals('outer'));
expect(roundTrip.f6, equals(<String>['a', 'b']));
- expect(roundTrip.f1[Int32(7)], equals(9.5));
+ expect(roundTrip.f1[(7)], equals(9.5));
expect(roundTrip.f3.name, equals('inner'));
expect(roundTrip.f5, equals(Color.blue));
});
diff --git a/dart/packages/fory-test/test/cross_lang_test/xlang_test_main.dart b/dart/packages/fory-test/test/cross_lang_test/xlang_test_main.dart
index a90e86a9e..f10848f35 100644
--- a/dart/packages/fory-test/test/cross_lang_test/xlang_test_main.dart
+++ b/dart/packages/fory-test/test/cross_lang_test/xlang_test_main.dart
@@ -75,6 +75,25 @@ void _roundTripFory(Fory fory, {bool trackRef = false}) {
_writeFile(output.takeBytes());
}
+void _runIntegerCase() {
+ final fory = _newFory(compatible: true);
+ registerXlangType(fory, Item1, id: 101);
+
+ final input = Buffer.wrap(_readFile());
+ final output = BytesBuilder(copy: false);
+
+ output.add(fory.serialize(fory.deserializeFrom<Item1>(input)));
+ for (var i = 0; i < 6; i += 1) {
+ final value = fory.deserializeFrom<int>(input);
+ output.add(fory.serializeBuiltin(value, wireTypeId: TypeIds.varInt32));
+ }
+
+ if (input.readableBytes != 0) {
+ throw StateError('Unexpected trailing bytes after integer payload.');
+ }
+ _writeFile(output.takeBytes());
+}
+
void _verifyBufferCase() {
final input = Buffer.wrap(_readFile());
final output = Buffer();
@@ -335,7 +354,7 @@ void _runCollectionElementRefRemoteTracking() {
registerXlangType(fory, RefOverrideContainer, id: 702);
final shared = RefOverrideElement()
- ..id = Int32(7)
+ ..id = 7
..name = 'shared_element';
// IMPORTANT: this peer intentionally writes a shared-reference payload with
// its default local ref-tracked schema. The Java reader uses ref-disabled
@@ -424,9 +443,7 @@ void _runCase(String caseName) {
_roundTripFory(fory);
return;
case 'test_integer':
- final fory = _newFory(compatible: true);
- registerXlangType(fory, Item1, id: 101);
- _roundTripFory(fory);
+ _runIntegerCase();
return;
case 'test_decimal':
_verifyDecimalCase();
@@ -452,6 +469,17 @@ void _runCase(String caseName) {
registerXlangType(fory, StructWithMap, id: 202);
_roundTripFory(fory);
return;
+ case 'test_nested_annotated_container_schema_consistent':
+ final fory = _newFory();
+ registerXlangType(fory, NestedAnnotatedContainerSchemaConsistent,
+ id: 801);
+ _roundTripFory(fory);
+ return;
+ case 'test_nested_annotated_container_compatible':
+ final fory = _newFory(compatible: true);
+ registerXlangType(fory, NestedAnnotatedContainerCompatible, id: 802);
+ _roundTripFory(fory);
+ return;
case 'test_skip_id_custom':
final fory = _newFory(compatible: true);
registerXlangType(fory, Color, id: 101);
diff --git a/dart/packages/fory-test/test/generated_round_trip_test.dart b/dart/packages/fory-test/test/generated_round_trip_test.dart
index 129912544..7d498fdd2 100644
--- a/dart/packages/fory-test/test/generated_round_trip_test.dart
+++ b/dart/packages/fory-test/test/generated_round_trip_test.dart
@@ -44,23 +44,23 @@ void main() {
final person = Person()
..name = 'Ada'
- ..age = Int32(36)
+ ..age = (36)
..favoriteColor = Color.blue
..tags = <String?>['engineer', null]
- ..scores = <String, Int32>{
- 'math': Int32(100),
- 'logic': Int32(99),
+ ..scores = <String, int>{
+ 'math': (100),
+ 'logic': (99),
};
final bytes = fory.serialize(person);
final roundTrip = fory.deserialize<Person>(bytes);
expect(roundTrip.name, equals('Ada'));
- expect(roundTrip.age, equals(Int32(36)));
+ expect(roundTrip.age, equals((36)));
expect(roundTrip.favoriteColor, equals(Color.blue));
expect(roundTrip.tags, equals(<String?>['engineer', null]));
- expect(roundTrip.scores['math'], equals(Int32(100)));
- expect(roundTrip.scores['logic'], equals(Int32(99)));
+ expect(roundTrip.scores['math'], equals((100)));
+ expect(roundTrip.scores['logic'], equals((99)));
});
test('supports root trackRef for top-level graphs', () {
@@ -171,14 +171,14 @@ void main() {
final person = Person()
..name = 'Ada'
- ..age = Int32(36)
+ ..age = (36)
..favoriteColor = Color.blue;
final bytes = fory.serialize(person);
final roundTrip = fory.deserialize<Person>(bytes);
expect(roundTrip.name, equals('Ada'));
- expect(roundTrip.age, equals(Int32(36)));
+ expect(roundTrip.age, equals((36)));
expect(roundTrip.favoriteColor, equals(Color.blue));
});
});
diff --git a/dart/packages/fory/README.md b/dart/packages/fory/README.md
index a44cbdd23..bacf4a2b4 100644
--- a/dart/packages/fory/README.md
+++ b/dart/packages/fory/README.md
@@ -14,9 +14,8 @@ cases.
- Compatible mode for schema evolution
- Optional reference tracking for shared and circular object graphs
- Manual serializers for external types, custom payloads, and unions
-- Explicit xlang value class such as `Int32`, `Int64`, `Uint32`, `Uint64`,
- `Float16`, `Bfloat16`, `Float32`, `LocalDate`, and `Timestamp`, plus `Duration`
- support
+- Explicit exact-width value classes for `Int64`, `Uint64`, `Float16`,
+ `Bfloat16`, `Float32`, `LocalDate`, and `Timestamp`, plus `Duration` support
## Getting Started
@@ -50,7 +49,9 @@ class Person {
Person();
String name = '';
- Int32 age = Int32(0);
+
+ @ForyField(type: Int32Type())
+ int age = 0;
Color favoriteColor = Color.red;
List<String> tags = <String>[];
}
@@ -73,7 +74,7 @@ void main() {
final person = Person()
..name = 'Ada'
- ..age = Int32(36)
+ ..age = 36
..favoriteColor = Color.blue
..tags = <String>['engineer', 'mathematician'];
@@ -172,6 +173,17 @@ class NodeList {
| `ref` | Enable reference tracking for this field |
| `dynamic` | Control whether runtime type metadata is written |
+`type:` is the canonical override surface for nested field semantics:
+
+```dart
+@MapField(
+ value: ListType(
+ element: Int32Type(encoding: Encoding.fixed),
+ ),
+)
+Map<String, List<int?>> nested = <String, List<int?>>{};
+```
+
## Customized Serializers
Use `Serializer<T>` when a type cannot use generated struct support or when you
@@ -222,49 +234,49 @@ void main() {
## Type Mapping
Dart has no native fixed-width 8/16/32-bit integer, unsigned 64-bit integer,
-or single-precision float types. Fory Dart provides thin wrapper types
-(`Int8`, `Int16`, `Int32`, `Int64`, `Uint8`, `Uint16`, `Uint32`, `Uint64`,
-`Float16`, `Bfloat16`, `Float32`) imported from `package:fory/fory.dart` to
-represent these xlang wire types. For 16-bit floating-point arrays, Dart exposes
-`Float16List` and `Bfloat16List` as contiguous fixed-length buffers.
-
-| Fory xlang type | Dart type |
-| --------------- | ------------------------- |
-| bool | `bool` |
-| int8 | `fory.Int8` (wrapper) |
-| int16 | `fory.Int16` (wrapper) |
-| int32 | `fory.Int32` (wrapper) |
-| int64 | `int` or `fory.Int64` |
-| uint8 | `fory.Uint8` (wrapper) |
-| uint16 | `fory.Uint16` (wrapper) |
-| uint32 | `fory.Uint32` (wrapper) |
-| uint64 | `fory.Uint64` (wrapper) |
-| float16 | `fory.Float16` (wrapper) |
-| bfloat16 | `fory.Bfloat16` (wrapper) |
-| float32 | `fory.Float32` (wrapper) |
-| float64 | `double` |
-| string | `String` |
-| binary | `Uint8List` |
-| duration | `Duration` |
-| local_date | `LocalDate` |
-| timestamp | `Timestamp` |
-| list | `List` |
-| set | `Set` |
-| map | `Map` |
-| enum | `enum` |
-| named_struct | `class` |
-| bool_array | `List<bool>` |
-| int8_array | `Int8List` |
-| int16_array | `Int16List` |
-| int32_array | `Int32List` |
-| int64_array | `Int64List` |
-| uint16_array | `Uint16List` |
-| uint32_array | `Uint32List` |
-| uint64_array | `Uint64List` |
-| float16_array | `Float16List` |
-| bfloat16_array | `Bfloat16List` |
-| float32_array | `Float32List` |
-| float64_array | `Float64List` |
+or single-precision float types. Fory Dart uses plain Dart `int` plus field
+annotations for 8/16/32-bit integer fields, keeps `Int64` and `Uint64` for
+full-range 64-bit values, and uses wrapper value types for reduced-width
+floating point. For 16-bit floating-point arrays, Dart exposes `Float16List`
+and `Bfloat16List` as contiguous fixed-length buffers.
+
+| Fory xlang type | Dart type |
+| --------------- | ---------------------------------------- |
+| bool | `bool` |
+| int8 | `int` + `@ForyField(type: Int8Type())` |
+| int16 | `int` + `@ForyField(type: Int16Type())` |
+| int32 | `int` + `@ForyField(type: Int32Type())` |
+| int64 | `int` or `fory.Int64` |
+| uint8 | `int` + `@ForyField(type: Uint8Type())` |
+| uint16 | `int` + `@ForyField(type: Uint16Type())` |
+| uint32 | `int` + `@ForyField(type: Uint32Type())` |
+| uint64 | `fory.Uint64` (wrapper) |
+| float16 | `fory.Float16` (wrapper) |
+| bfloat16 | `fory.Bfloat16` (wrapper) |
+| float32 | `fory.Float32` (wrapper) |
+| float64 | `double` |
+| string | `String` |
+| binary | `Uint8List` |
+| duration | `Duration` |
+| local_date | `LocalDate` |
+| timestamp | `Timestamp` |
+| list | `List` |
+| set | `Set` |
+| map | `Map` |
+| enum | `enum` |
+| named_struct | `class` |
+| bool_array | `List<bool>` |
+| int8_array | `Int8List` |
+| int16_array | `Int16List` |
+| int32_array | `Int32List` |
+| int64_array | `Int64List` |
+| uint16_array | `Uint16List` |
+| uint32_array | `Uint32List` |
+| uint64_array | `Uint64List` |
+| float16_array | `Float16List` |
+| bfloat16_array | `Bfloat16List` |
+| float32_array | `Float32List` |
+| float64_array | `Float64List` |
## Public API
@@ -272,16 +284,16 @@ The main exported API includes:
- `Fory` — main serialization facade
- `Config` — runtime configuration
-- `ForyStruct`, `ForyField` — struct annotations
+- `ForyStruct`, `ForyField`, `ListField`, `SetField`, `MapField` — struct annotations
- `ForyUnion` — union type annotation
- `Serializer`, `UnionSerializer`, `EnumSerializer` — serializer base classes
- `Buffer`, `WriteContext`, `ReadContext` — low-level I/O
-- `TypeSpec`, `ListType`, `MapType`, `ValueType` — nested container type
+- `TypeSpec`, `DeclaredType`, `ListType`, `SetType`, `MapType` — nested type
annotations
-- `Int32Type`, `Int64Type`, `Uint32Type`, `Uint64Type` — numeric encoding
- overrides
-- Numeric wrappers: `Int8`, `Int16`, `Int32`, `Int64`, `Uint8`, `Uint16`,
- `Uint32`, `Uint64`, `Float16`, `Bfloat16`, `Float32`
+- `Int8Type`, `Int16Type`, `Int32Type`, `Int64Type`, `Uint8Type`, `Uint16Type`,
+ `Uint32Type`, `Uint64Type`, `Float16Type`, `Bfloat16Type`, `Float32Type` —
+ scalar wire-type overrides
+- Numeric value wrappers: `Int64`, `Uint64`, `Float16`, `Bfloat16`, `Float32`
- Temporal types: `LocalDate`, `Timestamp`, `Duration`
## Cross-Language Notes
@@ -290,8 +302,8 @@ The main exported API includes:
- Register user-defined types before serialization or deserialization.
- Keep numeric IDs or `namespace + typeName` mappings consistent across
languages.
-- Use wrappers or numeric field annotations when the exact xlang wire type
- matters.
+- Use Dart `int` plus `@ForyField(type: ...)` for 8/16/32-bit integer fields,
+ and `Int64` / `Uint64` when full-range 64-bit values matter.
For the xlang wire format and type mapping details, see the
[Apache Fory specification](https://github.com/apache/fory/tree/main/docs/specification).
diff --git a/dart/packages/fory/example/example.dart b/dart/packages/fory/example/example.dart
index 6bb058e2f..c4214cbe2 100644
--- a/dart/packages/fory/example/example.dart
+++ b/dart/packages/fory/example/example.dart
@@ -31,7 +31,8 @@ class Person {
Person();
String name = '';
- Int32 age = Int32(0);
+ @ForyField(type: Int32Type())
+ int age = 0;
Color favoriteColor = Color.red;
List<String> tags = <String>[];
}
@@ -53,7 +54,7 @@ void main() {
final person = Person()
..name = 'Ada'
- ..age = Int32(36)
+ ..age = 36
..favoriteColor = Color.blue
..tags = <String>['engineer', 'mathematician'];
diff --git a/dart/packages/fory/lib/fory.dart b/dart/packages/fory/lib/fory.dart
index 5bc276e9b..74a7ba2b3 100644
--- a/dart/packages/fory/lib/fory.dart
+++ b/dart/packages/fory/lib/fory.dart
@@ -29,7 +29,6 @@ export 'src/annotation/fory_field.dart';
export 'src/annotation/fory_struct.dart';
export 'src/annotation/fory_union.dart';
export 'src/annotation/type_spec.dart';
-export 'src/annotation/numeric_types.dart';
export 'src/memory/buffer.dart'
hide
bufferByteData,
@@ -53,13 +52,7 @@ export 'src/types/bfloat16.dart';
export 'src/types/decimal.dart';
export 'src/types/float16.dart';
export 'src/types/float32.dart';
-export 'src/types/int16.dart';
-export 'src/types/int32.dart';
export 'src/types/int64.dart';
-export 'src/types/int8.dart';
export 'src/types/local_date.dart';
export 'src/types/timestamp.dart';
-export 'src/types/uint16.dart';
-export 'src/types/uint32.dart';
export 'src/types/uint64.dart';
-export 'src/types/uint8.dart';
diff --git a/dart/packages/fory/lib/src/annotation/fory_field.dart b/dart/packages/fory/lib/src/annotation/fory_field.dart
index 555144c96..217689166 100644
--- a/dart/packages/fory/lib/src/annotation/fory_field.dart
+++ b/dart/packages/fory/lib/src/annotation/fory_field.dart
@@ -17,39 +17,78 @@
* under the License.
*/
-/// Field-level code generation options for [ForyStruct].
+import 'package:fory/src/annotation/type_spec.dart';
+
final class ForyField {
- /// Skips this field entirely in generated serializers.
final bool skip;
-
- /// Stable field identifier used by compatible structs.
- ///
- /// When omitted, the generator derives an identifier from the field name.
final int? id;
+ final bool? nullable;
+ final bool ref;
+ final bool? dynamic;
+ final TypeSpec? type;
+
+ const ForyField({
+ this.skip = false,
+ this.id,
+ this.nullable,
+ this.ref = false,
+ this.dynamic,
+ this.type,
+ });
+}
- /// Overrides the generator's inferred nullability.
- ///
- /// `null` means "use the Dart type as written".
+final class ListField {
+ final bool skip;
+ final int? id;
final bool? nullable;
+ final bool ref;
+ final bool? dynamic;
+ final TypeSpec? element;
- /// Enables reference tracking for this field.
- ///
- /// Basic scalar types never track references even if this flag is `true`.
+ const ListField({
+ this.skip = false,
+ this.id,
+ this.nullable,
+ this.ref = false,
+ this.dynamic,
+ this.element,
+ });
+}
+
+final class SetField {
+ final bool skip;
+ final int? id;
+ final bool? nullable;
final bool ref;
+ final bool? dynamic;
+ final TypeSpec? element;
- /// Controls whether generated code writes runtime type metadata for this
- /// field.
- ///
- /// `null` means "auto", `false` means "use the declared type", and `true`
- /// means "write runtime type information".
+ const SetField({
+ this.skip = false,
+ this.id,
+ this.nullable,
+ this.ref = false,
+ this.dynamic,
+ this.element,
+ });
+}
+
+final class MapField {
+ final bool skip;
+ final int? id;
+ final bool? nullable;
+ final bool ref;
final bool? dynamic;
+ final TypeSpec? key;
+ final TypeSpec? value;
- /// Creates field-level generation overrides.
- const ForyField({
+ const MapField({
this.skip = false,
this.id,
this.nullable,
this.ref = false,
this.dynamic,
+ this.key,
+ this.value,
});
}
diff --git a/dart/packages/fory/lib/src/annotation/numeric_types.dart b/dart/packages/fory/lib/src/annotation/numeric_types.dart
deleted file mode 100644
index d965b34ca..000000000
--- a/dart/packages/fory/lib/src/annotation/numeric_types.dart
+++ /dev/null
@@ -1,80 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-/// Encodings available for 64-bit signed and unsigned integer fields.
-enum LongEncoding {
- /// Always use the fixed-width 64-bit wire form.
- fixed,
-
- /// Use variable-length varint encoding.
- varint,
-
- /// Use the tagged compact-or-wide 64-bit encoding.
- tagged,
-}
-
-/// Overrides the generated wire encoding for a 32-bit signed integer field.
-final class Int32Type {
- /// Whether to use the compressed varint representation instead of fixed-width
- /// `int32`.
- final bool compress;
-
- /// Creates an `int32` encoding override.
- const Int32Type({this.compress = true});
-}
-
-/// Overrides the generated wire encoding for a 64-bit signed integer field.
-final class Int64Type {
- /// Selected encoding for the field.
- final LongEncoding encoding;
-
- /// Creates an `int64` encoding override.
- const Int64Type({this.encoding = LongEncoding.varint});
-}
-
-/// Marks a field as xlang `uint8`.
-final class Uint8Type {
- /// Creates a `uint8` encoding override.
- const Uint8Type();
-}
-
-/// Marks a field as xlang `uint16`.
-final class Uint16Type {
- /// Creates a `uint16` encoding override.
- const Uint16Type();
-}
-
-/// Overrides the generated wire encoding for a 32-bit unsigned integer field.
-final class Uint32Type {
- /// Whether to use the compressed varuint representation instead of fixed-width
- /// `uint32`.
- final bool compress;
-
- /// Creates a `uint32` encoding override.
- const Uint32Type({this.compress = true});
-}
-
-/// Overrides the generated wire encoding for a 64-bit unsigned integer field.
-final class Uint64Type {
- /// Selected encoding for the field.
- final LongEncoding encoding;
-
- /// Creates a `uint64` encoding override.
- const Uint64Type({this.encoding = LongEncoding.varint});
-}
diff --git a/dart/packages/fory/lib/src/annotation/type_spec.dart b/dart/packages/fory/lib/src/annotation/type_spec.dart
index 189ab4159..5649e0339 100644
--- a/dart/packages/fory/lib/src/annotation/type_spec.dart
+++ b/dart/packages/fory/lib/src/annotation/type_spec.dart
@@ -17,95 +17,173 @@
* under the License.
*/
-/// Type-level annotations for configuring nested container elements.
-///
-/// Use [ListType] and [MapType] on fields to override default ref-tracking
-/// and nullability for container elements, keys, and values.
library;
-/// Option that modifies a type's serialization behavior.
-abstract class TypeOption {
- const TypeOption();
+enum Encoding { fixed, varint, tagged }
- const factory TypeOption.ref([bool tracked]) = RefOption;
- const factory TypeOption.nullable([bool value]) = NullableOption;
+abstract class TypeSpec {
+ final bool? nullable;
+ final bool? ref;
+ final bool? dynamic;
+
+ const TypeSpec({
+ this.nullable,
+ this.ref,
+ this.dynamic,
+ });
}
-/// Enables or disables reference tracking for a type.
-final class RefOption extends TypeOption {
- final bool tracked;
- const RefOption([this.tracked = true]);
+abstract class ScalarTypeSpec extends TypeSpec {
+ const ScalarTypeSpec({
+ super.nullable,
+ super.ref,
+ super.dynamic,
+ });
}
-/// Overrides nullability for a type.
-final class NullableOption extends TypeOption {
- final bool value;
- const NullableOption([this.value = true]);
+final class DeclaredType extends TypeSpec {
+ const DeclaredType({
+ super.nullable,
+ super.ref,
+ super.dynamic,
+ });
}
-/// Base class for type specifications that carry [TypeOption]s.
-abstract class TypeSpec {
- final List<TypeOption> options;
- const TypeSpec([this.options = const []]);
+final class BoolType extends ScalarTypeSpec {
+ const BoolType({super.nullable, super.ref, super.dynamic});
+}
+
+final class Int8Type extends ScalarTypeSpec {
+ const Int8Type({super.nullable, super.ref, super.dynamic});
+}
+
+final class Int16Type extends ScalarTypeSpec {
+ const Int16Type({super.nullable, super.ref, super.dynamic});
}
-/// Specifies options for a scalar or object value type.
-final class ValueType extends TypeSpec {
- const ValueType([super.options]);
+final class Int32Type extends ScalarTypeSpec {
+ final Encoding encoding;
+
+ const Int32Type({
+ this.encoding = Encoding.varint,
+ super.nullable,
+ super.ref,
+ super.dynamic,
+ });
+}
+
+final class Int64Type extends ScalarTypeSpec {
+ final Encoding encoding;
+
+ const Int64Type({
+ this.encoding = Encoding.varint,
+ super.nullable,
+ super.ref,
+ super.dynamic,
+ });
+}
+
+final class Uint8Type extends ScalarTypeSpec {
+ const Uint8Type({super.nullable, super.ref, super.dynamic});
+}
+
+final class Uint16Type extends ScalarTypeSpec {
+ const Uint16Type({super.nullable, super.ref, super.dynamic});
+}
+
+final class Uint32Type extends ScalarTypeSpec {
+ final Encoding encoding;
+
+ const Uint32Type({
+ this.encoding = Encoding.varint,
+ super.nullable,
+ super.ref,
+ super.dynamic,
+ });
+}
+
+final class Uint64Type extends ScalarTypeSpec {
+ final Encoding encoding;
+
+ const Uint64Type({
+ this.encoding = Encoding.varint,
+ super.nullable,
+ super.ref,
+ super.dynamic,
+ });
+}
- const ValueType.ref() : super(const [TypeOption.ref()]);
- const ValueType.noRef() : super(const [TypeOption.ref(false)]);
- const ValueType.nullable() : super(const [TypeOption.nullable()]);
- const ValueType.nonNullable() : super(const [TypeOption.nullable(false)]);
- const ValueType.refNullable()
- : super(const [TypeOption.ref(), TypeOption.nullable()]);
+final class Float16Type extends ScalarTypeSpec {
+ const Float16Type({super.nullable, super.ref, super.dynamic});
+}
+
+final class Bfloat16Type extends ScalarTypeSpec {
+ const Bfloat16Type({super.nullable, super.ref, super.dynamic});
+}
+
+final class Float32Type extends ScalarTypeSpec {
+ const Float32Type({super.nullable, super.ref, super.dynamic});
+}
+
+final class Float64Type extends ScalarTypeSpec {
+ const Float64Type({super.nullable, super.ref, super.dynamic});
+}
+
+final class StringType extends ScalarTypeSpec {
+ const StringType({super.nullable, super.ref, super.dynamic});
+}
+
+final class DecimalType extends ScalarTypeSpec {
+ const DecimalType({super.nullable, super.ref, super.dynamic});
+}
+
+final class TimestampType extends ScalarTypeSpec {
+ const TimestampType({super.nullable, super.ref, super.dynamic});
+}
+
+final class DateType extends ScalarTypeSpec {
+ const DateType({super.nullable, super.ref, super.dynamic});
+}
+
+final class DurationType extends ScalarTypeSpec {
+ const DurationType({super.nullable, super.ref, super.dynamic});
+}
+
+final class BinaryType extends ScalarTypeSpec {
+ const BinaryType({super.nullable, super.ref, super.dynamic});
}
-/// Specifies options for a list or set field, including its element type.
final class ListType extends TypeSpec {
- final TypeSpec element;
+ final TypeSpec? element;
const ListType({
- this.element = const ValueType(),
- List<TypeOption> options = const [],
- }) : super(options);
-
- const ListType.ref({
- this.element = const ValueType(),
- }) : super(const [TypeOption.ref()]);
+ this.element,
+ super.nullable,
+ super.ref,
+ super.dynamic,
+ });
+}
- const ListType.noRef({
- this.element = const ValueType(),
- }) : super(const [TypeOption.ref(false)]);
+final class SetType extends TypeSpec {
+ final TypeSpec? element;
- const ListType.nullable({
- this.element = const ValueType(),
- }) : super(const [TypeOption.nullable()]);
+ const SetType({
+ this.element,
+ super.nullable,
+ super.ref,
+ super.dynamic,
+ });
}
-/// Specifies options for a map field, including its key and value types.
final class MapType extends TypeSpec {
- final TypeSpec key;
- final TypeSpec value;
+ final TypeSpec? key;
+ final TypeSpec? value;
const MapType({
- this.key = const ValueType(),
- this.value = const ValueType(),
- List<TypeOption> options = const [],
- }) : super(options);
-
- const MapType.ref({
- this.key = const ValueType(),
- this.value = const ValueType(),
- }) : super(const [TypeOption.ref()]);
-
- const MapType.noRef({
- this.key = const ValueType(),
- this.value = const ValueType(),
- }) : super(const [TypeOption.ref(false)]);
-
- const MapType.nullable({
- this.key = const ValueType(),
- this.value = const ValueType(),
- }) : super(const [TypeOption.nullable()]);
+ this.key,
+ this.value,
+ super.nullable,
+ super.ref,
+ super.dynamic,
+ });
}
diff --git a/dart/packages/fory/lib/src/codegen/fory_generator.dart b/dart/packages/fory/lib/src/codegen/fory_generator.dart
index ae20f05ab..0b1d9683d 100644
--- a/dart/packages/fory/lib/src/codegen/fory_generator.dart
+++ b/dart/packages/fory/lib/src/codegen/fory_generator.dart
@@ -19,6 +19,7 @@
import 'dart:async';
+import 'package:analyzer/dart/constant/value.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/dart/element/nullability_suffix.dart';
import 'package:analyzer/dart/element/type.dart';
@@ -53,23 +54,21 @@ final class ForyGenerator extends Generator {
static final TypeChecker _foryFieldChecker = TypeChecker.fromRuntime(
ForyField,
);
- static final TypeChecker _foryUnionChecker = TypeChecker.fromRuntime(
- ForyUnion,
+ static final TypeChecker _listFieldChecker = TypeChecker.fromRuntime(
+ ListField,
);
- static final TypeChecker _listTypeChecker = TypeChecker.fromRuntime(ListType);
- static final TypeChecker _mapTypeChecker = TypeChecker.fromRuntime(MapType);
- static final TypeChecker _refOptionChecker = TypeChecker.fromRuntime(
- RefOption,
+ static final TypeChecker _setFieldChecker = TypeChecker.fromRuntime(
+ SetField,
);
- static final TypeChecker _nullableOptionChecker = TypeChecker.fromRuntime(
- NullableOption,
+ static final TypeChecker _mapFieldChecker = TypeChecker.fromRuntime(
+ MapField,
+ );
+ static final TypeChecker _typeSpecChecker = TypeChecker.fromRuntime(
+ TypeSpec,
+ );
+ static final TypeChecker _foryUnionChecker = TypeChecker.fromRuntime(
+ ForyUnion,
);
- static final TypeChecker _int32Checker = TypeChecker.fromRuntime(Int32Type);
- static final TypeChecker _int64Checker = TypeChecker.fromRuntime(Int64Type);
- static final TypeChecker _uint8Checker = TypeChecker.fromRuntime(Uint8Type);
- static final TypeChecker _uint16Checker = TypeChecker.fromRuntime(Uint16Type);
- static final TypeChecker _uint32Checker = TypeChecker.fromRuntime(Uint32Type);
- static final TypeChecker _uint64Checker = TypeChecker.fromRuntime(Uint64Type);
final Map<String, String> _importPrefixByLibraryIdentifier =
<String, String>{};
@@ -159,7 +158,7 @@ final class ForyGenerator extends Generator {
}
_GeneratedFieldSpec _analyzeField(FieldElement field) {
- final annotation = _foryFieldChecker.firstAnnotationOf(field);
+ final annotation = _fieldAnnotationOf(field);
final reader = annotation == null ? null : ConstantReader(annotation);
final idValue = reader?.peek('id');
final nullableValue = reader?.peek('nullable');
@@ -172,9 +171,7 @@ final class ForyGenerator extends Generator {
? _autoDynamic(field.type)
: dynamicValue.boolValue;
final ref = reader?.peek('ref')?.boolValue ?? false;
-
- // Read container TypeSpec annotations (@ListType / @MapType).
- final typeSpec = _analyzeTypeSpecAnnotation(field);
+ final typeSpec = _analyzeTypeSpecAnnotation(field, reader);
return _GeneratedFieldSpec(
name: field.displayName,
@@ -194,7 +191,7 @@ final class ForyGenerator extends Generator {
ref: ref,
dynamic: dynamic,
typeSpec: typeSpec,
- integerAnnotation: _analyzeIntegerAnnotation(field),
+ errorElement: field,
),
);
}
@@ -205,70 +202,107 @@ final class ForyGenerator extends Generator {
required bool ref,
required bool? dynamic,
_TypeSpecInfo? typeSpec,
- _IntegerAnnotationSpec? integerAnnotation,
+ required Element errorElement,
}) {
- // If a TypeSpec annotation overrides this level, apply it.
if (typeSpec != null) {
final specRef = typeSpec.ref;
if (specRef != null) ref = specRef;
final specNullable = typeSpec.nullable;
if (specNullable != null) nullable = specNullable;
+ final specDynamic = typeSpec.dynamic;
+ if (specDynamic != null) dynamic = specDynamic;
+ }
+ if (typeSpec == null && _isDefaultBoolArrayCarrier(type)) {
+ return _GeneratedFieldTypeSpec(
+ typeLiteral: _typeReferenceLiteral(type),
+ declaredTypeName: _typeReferenceLiteral(type),
+ typeId: TypeIds.boolArray,
+ nullable: nullable,
+ ref: ref,
+ dynamic: dynamic,
+ arguments: const <_GeneratedFieldTypeSpec>[],
+ );
}
if (_isList(type) || _isSet(type)) {
+ final expectedTypeId = _isSet(type) ? TypeIds.set : TypeIds.list;
+ if (typeSpec?.typeId != null && typeSpec!.typeId != expectedTypeId) {
+ throw InvalidGenerationSourceError(
+ 'Type override ${_typeSpecName(typeSpec.typeId!)} does not match '
+ 'the declared ${_isSet(type) ? 'Set' : 'List'} carrier.',
+ element: errorElement,
+ );
+ }
final argument = (type as InterfaceType).typeArguments.single;
- final elementSpec =
- typeSpec is _ListTypeSpecInfo ? typeSpec.element : null;
+ final elementSpec = typeSpec?.element;
+ final child = _fieldTypeForType(
+ argument,
+ nullable: _isNullable(argument),
+ ref: false,
+ dynamic: _autoDynamic(argument),
+ typeSpec: elementSpec,
+ errorElement: errorElement,
+ );
+ _validatePackedListCarrier(type, child, errorElement);
return _GeneratedFieldTypeSpec(
typeLiteral: _typeReferenceLiteral(type),
declaredTypeName: _typeReferenceLiteral(type),
- typeId: _typeIdFor(type),
+ typeId: expectedTypeId,
nullable: nullable,
ref: ref,
dynamic: dynamic,
- arguments: <_GeneratedFieldTypeSpec>[
- _fieldTypeForType(
- argument,
- nullable: true,
- ref: elementSpec?.ref ?? false,
- dynamic: _autoDynamic(argument),
- typeSpec: elementSpec,
- ),
- ],
+ arguments: <_GeneratedFieldTypeSpec>[child],
);
}
if (_isMap(type)) {
+ if (typeSpec?.typeId != null && typeSpec!.typeId != TypeIds.map) {
+ throw InvalidGenerationSourceError(
+ 'Type override ${_typeSpecName(typeSpec.typeId!)} does not match '
+ 'the declared Map carrier.',
+ element: errorElement,
+ );
+ }
final arguments = (type as InterfaceType).typeArguments;
- final keySpec = typeSpec is _MapTypeSpecInfo ? typeSpec.key : null;
- final valueSpec = typeSpec is _MapTypeSpecInfo ? typeSpec.value : null;
+ final keySpec = typeSpec?.key;
+ final valueSpec = typeSpec?.value;
return _GeneratedFieldTypeSpec(
typeLiteral: _typeReferenceLiteral(type),
declaredTypeName: _typeReferenceLiteral(type),
- typeId: _typeIdFor(type),
+ typeId: TypeIds.map,
nullable: nullable,
ref: ref,
dynamic: dynamic,
arguments: <_GeneratedFieldTypeSpec>[
_fieldTypeForType(
arguments[0],
- nullable: true,
- ref: keySpec?.ref ?? false,
+ nullable: _isNullable(arguments[0]),
+ ref: false,
dynamic: _autoDynamic(arguments[0]),
typeSpec: keySpec,
+ errorElement: errorElement,
),
_fieldTypeForType(
arguments[1],
- nullable: true,
- ref: valueSpec?.ref ?? false,
+ nullable: _isNullable(arguments[1]),
+ ref: false,
dynamic: _autoDynamic(arguments[1]),
typeSpec: valueSpec,
+ errorElement: errorElement,
),
],
);
}
+ final typeId = typeSpec?.typeId ?? _typeIdFor(type);
+ _validateScalarTypeOverride(
+ type,
+ typeId,
+ dynamic,
+ typeSpec,
+ errorElement,
+ );
return _GeneratedFieldTypeSpec(
typeLiteral: _typeReferenceLiteral(type),
declaredTypeName: _typeReferenceLiteral(type),
- typeId: _typeIdFor(type, integerAnnotation: integerAnnotation),
+ typeId: typeId,
nullable: nullable,
ref: ref,
dynamic: dynamic,
@@ -870,7 +904,7 @@ final class ForyGenerator extends Generator {
}
bool _isSkipped(FieldElement field) {
- final annotation = _foryFieldChecker.firstAnnotationOf(field);
+ final annotation = _fieldAnnotationOf(field);
if (annotation == null) {
return false;
}
@@ -992,6 +1026,9 @@ GeneratedFieldType(
String valueExpression,
) {
if (_isList(type)) {
+ if (fieldType.typeId != TypeIds.list) {
+ return '$valueExpression as ${_typeCodeString(type)}';
+ }
final elementType = (type as InterfaceType).typeArguments.single;
final elementFieldType = fieldType.arguments.single;
if (_supportsDirectContainerCast(elementType, elementFieldType)) {
@@ -1006,6 +1043,9 @@ GeneratedFieldType(
return 'List<${_typeCodeString(elementType)}>.of((($valueExpression as List)).map((item) => $convertedElement))';
}
if (_isSet(type)) {
+ if (fieldType.typeId != TypeIds.set) {
+ return '$valueExpression as ${_typeCodeString(type)}';
+ }
final elementType = (type as InterfaceType).typeArguments.single;
final elementFieldType = fieldType.arguments.single;
if (_supportsDirectContainerCast(elementType, elementFieldType)) {
@@ -1045,24 +1085,10 @@ GeneratedFieldType(
}
if (type.isDartCoreInt) {
switch (fieldType.typeId) {
- case TypeIds.int8:
- return 'switch ($valueExpression) { int typed => typed, Int8 typed => typed.value, _ => throw StateError(\'Expected int or Int8.\') }';
- case TypeIds.int16:
- return 'switch ($valueExpression) { int typed => typed, Int16 typed => typed.value, _ => throw StateError(\'Expected int or Int16.\') }';
- case TypeIds.int32:
- case TypeIds.varInt32:
- return 'switch ($valueExpression) { int typed => typed, Int32 typed => typed.value, _ => throw StateError(\'Expected int or Int32.\') }';
case TypeIds.int64:
case TypeIds.varInt64:
case TypeIds.taggedInt64:
return 'switch ($valueExpression) { Int64 typed => typed.toInt(), int typed => typed, _ => throw StateError(\'Expected int or Int64.\') }';
- case TypeIds.uint8:
- return 'switch ($valueExpression) { int typed => typed, Uint8 typed => typed.value, _ => throw StateError(\'Expected int or Uint8.\') }';
- case TypeIds.uint16:
- return 'switch ($valueExpression) { int typed => typed, Uint16 typed => typed.value, _ => throw StateError(\'Expected int or Uint16.\') }';
- case TypeIds.uint32:
- case TypeIds.varUint32:
- return 'switch ($valueExpression) { int typed => typed, Uint32 typed => typed.value, _ => throw StateError(\'Expected int or Uint32.\') }';
case TypeIds.uint64:
case TypeIds.varUint64:
case TypeIds.taggedUint64:
@@ -1100,8 +1126,7 @@ GeneratedFieldType(
return false;
}
if (type.isDartCoreInt) {
- return fieldType.typeId != TypeIds.int32 &&
- fieldType.typeId != TypeIds.varInt32;
+ return true;
}
return true;
}
@@ -1444,17 +1469,13 @@ GeneratedFieldType(
case TypeIds.boolType:
return 'buffer.readBool()';
case TypeIds.int8:
- return 'Int8(buffer.readByte())';
+ return 'buffer.readByte()';
case TypeIds.int16:
- return 'Int16(buffer.readInt16())';
+ return 'buffer.readInt16()';
case TypeIds.int32:
- return field.type.isDartCoreInt
- ? 'buffer.readInt32()'
- : 'Int32(buffer.readInt32())';
+ return 'buffer.readInt32()';
case TypeIds.varInt32:
- return field.type.isDartCoreInt
- ? 'buffer.readVarInt32()'
- : 'Int32(buffer.readVarInt32())';
+ return 'buffer.readVarInt32()';
case TypeIds.int64:
return field.type.isDartCoreInt
? 'buffer.readInt64AsInt()'
@@ -1468,21 +1489,13 @@ GeneratedFieldType(
? 'buffer.readTaggedInt64AsInt()'
: 'buffer.readTaggedInt64()';
case TypeIds.uint8:
- return field.type.isDartCoreInt
- ? 'buffer.readUint8()'
- : 'Uint8(buffer.readUint8())';
+ return 'buffer.readUint8()';
case TypeIds.uint16:
- return field.type.isDartCoreInt
- ? 'buffer.readUint16()'
- : 'Uint16(buffer.readUint16())';
+ return 'buffer.readUint16()';
case TypeIds.uint32:
- return field.type.isDartCoreInt
- ? 'buffer.readUint32()'
- : 'Uint32(buffer.readUint32())';
+ return 'buffer.readUint32()';
case TypeIds.varUint32:
- return field.type.isDartCoreInt
- ? 'buffer.readVarUint32()'
- : 'Uint32(buffer.readVarUint32())';
+ return 'buffer.readVarUint32()';
case TypeIds.uint64:
return field.type.isDartCoreInt
? 'buffer.readUint64().toInt()'
@@ -1562,17 +1575,13 @@ GeneratedFieldType(
case TypeIds.boolType:
return '$cursorExpression.readBool()';
case TypeIds.int8:
- return 'Int8($cursorExpression.readByte())';
+ return '$cursorExpression.readByte()';
case TypeIds.int16:
- return 'Int16($cursorExpression.readInt16())';
+ return '$cursorExpression.readInt16()';
case TypeIds.int32:
- return field.type.isDartCoreInt
- ? '$cursorExpression.readInt32()'
- : 'Int32($cursorExpression.readInt32())';
+ return '$cursorExpression.readInt32()';
case TypeIds.varInt32:
- return field.type.isDartCoreInt
- ? '$cursorExpression.readVarInt32()'
- : 'Int32($cursorExpression.readVarInt32())';
+ return '$cursorExpression.readVarInt32()';
case TypeIds.int64:
return field.type.isDartCoreInt
? '$cursorExpression.readInt64AsInt()'
@@ -1586,21 +1595,13 @@ GeneratedFieldType(
? '$cursorExpression.readTaggedInt64AsInt()'
: '$cursorExpression.readTaggedInt64()';
case TypeIds.uint8:
- return field.type.isDartCoreInt
- ? '$cursorExpression.readUint8()'
- : 'Uint8($cursorExpression.readUint8())';
+ return '$cursorExpression.readUint8()';
case TypeIds.uint16:
- return field.type.isDartCoreInt
- ? '$cursorExpression.readUint16()'
- : 'Uint16($cursorExpression.readUint16())';
+ return '$cursorExpression.readUint16()';
case TypeIds.uint32:
- return field.type.isDartCoreInt
- ? '$cursorExpression.readUint32()'
- : 'Uint32($cursorExpression.readUint32())';
+ return '$cursorExpression.readUint32()';
case TypeIds.varUint32:
- return field.type.isDartCoreInt
- ? '$cursorExpression.readVarUint32()'
- : 'Uint32($cursorExpression.readVarUint32())';
+ return '$cursorExpression.readVarUint32()';
case TypeIds.uint64:
return field.type.isDartCoreInt
? '$cursorExpression.readUint64AsInt()'
@@ -1677,7 +1678,8 @@ GeneratedFieldType(
case TypeIds.taggedUint64:
return 'Uint64($valueExpression)';
default:
- return valueExpression;
+ return _checkedGeneratedScalarExpression(
+ field.fieldType.typeId, valueExpression);
}
}
if (field.type.isDartCoreDouble ||
@@ -1703,24 +1705,8 @@ GeneratedFieldType(
String _generatedFieldInfoWriteValueExpression(
_GeneratedFieldSpec field,
String valueExpression,
- ) {
- if (!_withoutNullability(field.type).isDartCoreInt) {
- return valueExpression;
- }
- final wrapper = switch (field.fieldType.typeId) {
- TypeIds.int8 => 'Int8',
- TypeIds.int16 => 'Int16',
- TypeIds.int32 || TypeIds.varInt32 => 'Int32',
- _ => null,
- };
- if (wrapper == null) {
- return valueExpression;
- }
- if (_isNullable(field.type)) {
- return '$valueExpression == null ? null : $wrapper($valueExpression!)';
- }
- return '$wrapper($valueExpression)';
- }
+ ) =>
+ valueExpression;
String _nullExpression(
DartType type, {
@@ -2031,148 +2017,566 @@ GeneratedFieldType(
}
}
- _IntegerAnnotationSpec? _analyzeIntegerAnnotation(FieldElement field) {
- final int32Annotation = _int32Checker.firstAnnotationOf(field);
- if (int32Annotation != null) {
- final reader = ConstantReader(int32Annotation);
- final compress = reader.peek('compress')?.boolValue ?? true;
- return _IntegerAnnotationSpec(
- typeId: compress ? TypeIds.varInt32 : TypeIds.int32,
+ DartObject? _fieldAnnotationOf(FieldElement field) {
+ for (final metadata in field.metadata) {
+ final annotation = metadata.computeConstantValue();
+ final annotationType = annotation?.type;
+ if (annotationType != null &&
+ _typeSpecChecker.isAssignableFromType(annotationType)) {
+ throw InvalidGenerationSourceError(
+ 'Standalone type-spec annotations like @${annotationType.element?.displayName ?? 'TypeSpec'}() '
+ 'are not supported. Use @ForyField(type: ...) or container field sugar instead.',
+ element: field,
+ );
+ }
+ }
+ final annotations = <DartObject?>[
+ _foryFieldChecker.firstAnnotationOf(field),
+ _listFieldChecker.firstAnnotationOf(field),
+ _setFieldChecker.firstAnnotationOf(field),
+ _mapFieldChecker.firstAnnotationOf(field),
+ ].whereType<DartObject>().toList(growable: false);
+ if (annotations.length > 1) {
+ throw InvalidGenerationSourceError(
+ 'Use only one of @ForyField, @ListField, @SetField, or @MapField on a field.',
+ element: field,
);
}
- final int64Annotation = _int64Checker.firstAnnotationOf(field);
- if (int64Annotation != null) {
- final reader = ConstantReader(int64Annotation);
- final encodingReader = reader.peek('encoding');
- final encodingValue = encodingReader == null || encodingReader.isNull
- ? 'varint'
- : encodingReader.revive().accessor.split('.').last;
- final typeId = switch (encodingValue) {
- 'varint' => TypeIds.varInt64,
- 'tagged' => TypeIds.taggedInt64,
- 'fixed' => TypeIds.int64,
- _ => throw InvalidGenerationSourceError(
- 'Unsupported Int64Type encoding: $encodingValue.',
- element: field,
- ),
- };
- return _IntegerAnnotationSpec(typeId: typeId);
- }
- final uint8Annotation = _uint8Checker.firstAnnotationOf(field);
- if (uint8Annotation != null) {
- return const _IntegerAnnotationSpec(typeId: TypeIds.uint8);
- }
- final uint16Annotation = _uint16Checker.firstAnnotationOf(field);
- if (uint16Annotation != null) {
- return const _IntegerAnnotationSpec(typeId: TypeIds.uint16);
- }
- final uint32Annotation = _uint32Checker.firstAnnotationOf(field);
- if (uint32Annotation != null) {
- final reader = ConstantReader(uint32Annotation);
- final compress = reader.peek('compress')?.boolValue ?? true;
- return _IntegerAnnotationSpec(
- typeId: compress ? TypeIds.varUint32 : TypeIds.uint32,
+ return annotations.isEmpty ? null : annotations.single;
+ }
+
+ _TypeSpecInfo? _analyzeTypeSpecAnnotation(
+ FieldElement field,
+ ConstantReader? reader,
+ ) {
+ final annotation = _fieldAnnotationOf(field);
+ if (annotation == null || reader == null) {
+ return null;
+ }
+ final annotationType = annotation.type;
+ if (annotationType != null &&
+ _foryFieldChecker.isExactlyType(annotationType)) {
+ final typeReader = reader.peek('type');
+ if (typeReader == null || typeReader.isNull) {
+ return null;
+ }
+ final typeSpec = _readTypeSpecObj(typeReader, field);
+ _validateRootTypeSpecConflicts(field, reader, typeSpec);
+ return typeSpec;
+ }
+ if (annotationType != null &&
+ _listFieldChecker.isExactlyType(annotationType)) {
+ return _TypeSpecInfo(
+ typeId: TypeIds.list,
+ element: _readOptionalTypeSpec(reader.peek('element'), field),
);
}
- final uint64Annotation = _uint64Checker.firstAnnotationOf(field);
- if (uint64Annotation != null) {
- final reader = ConstantReader(uint64Annotation);
- final encodingReader = reader.peek('encoding');
- final encodingValue = encodingReader == null || encodingReader.isNull
- ? 'varint'
- : encodingReader.revive().accessor.split('.').last;
- final typeId = switch (encodingValue) {
- 'varint' => TypeIds.varUint64,
- 'tagged' => TypeIds.taggedUint64,
- 'fixed' => TypeIds.uint64,
- _ => throw InvalidGenerationSourceError(
- 'Unsupported Uint64Type encoding: $encodingValue.',
- element: field,
- ),
- };
- return _IntegerAnnotationSpec(typeId: typeId);
+ if (annotationType != null &&
+ _setFieldChecker.isExactlyType(annotationType)) {
+ return _TypeSpecInfo(
+ typeId: TypeIds.set,
+ element: _readOptionalTypeSpec(reader.peek('element'), field),
+ );
+ }
+ if (annotationType != null &&
+ _mapFieldChecker.isExactlyType(annotationType)) {
+ return _TypeSpecInfo(
+ typeId: TypeIds.map,
+ key: _readOptionalTypeSpec(reader.peek('key'), field),
+ value: _readOptionalTypeSpec(reader.peek('value'), field),
+ );
}
return null;
}
- _TypeSpecInfo? _analyzeTypeSpecAnnotation(FieldElement field) {
- final listAnnotation = _listTypeChecker.firstAnnotationOf(field);
- if (listAnnotation != null) {
- return _readListTypeSpec(ConstantReader(listAnnotation));
+ _TypeSpecInfo? _readOptionalTypeSpec(ConstantReader? reader, Element field) {
+ if (reader == null || reader.isNull) {
+ return null;
}
- final mapAnnotation = _mapTypeChecker.firstAnnotationOf(field);
- if (mapAnnotation != null) {
- return _readMapTypeSpec(ConstantReader(mapAnnotation));
+ return _readTypeSpecObj(reader, field);
+ }
+
+ _TypeSpecInfo _readTypeSpecObj(ConstantReader reader, Element field) {
+ final objType = reader.objectValue.type;
+ final typeName = objType?.element?.displayName;
+ final nullable = _readBoolOverride(reader.peek('nullable'));
+ final ref = _readBoolOverride(reader.peek('ref'));
+ final dynamic = _readBoolOverride(reader.peek('dynamic'));
+ switch (typeName) {
+ case 'DeclaredType':
+ return _TypeSpecInfo(
+ nullable: nullable,
+ ref: ref,
+ dynamic: dynamic,
+ );
+ case 'ListType':
+ return _TypeSpecInfo(
+ typeId: TypeIds.list,
+ nullable: nullable,
+ ref: ref,
+ dynamic: dynamic,
+ element: _readOptionalTypeSpec(reader.peek('element'), field),
+ );
+ case 'SetType':
+ return _TypeSpecInfo(
+ typeId: TypeIds.set,
+ nullable: nullable,
+ ref: ref,
+ dynamic: dynamic,
+ element: _readOptionalTypeSpec(reader.peek('element'), field),
+ );
+ case 'MapType':
+ return _TypeSpecInfo(
+ typeId: TypeIds.map,
+ nullable: nullable,
+ ref: ref,
+ dynamic: dynamic,
+ key: _readOptionalTypeSpec(reader.peek('key'), field),
+ value: _readOptionalTypeSpec(reader.peek('value'), field),
+ );
+ case 'BoolType':
+ return _TypeSpecInfo(
+ typeId: TypeIds.boolType,
+ nullable: nullable,
+ ref: ref,
+ dynamic: dynamic,
+ );
+ case 'Int8Type':
+ return _TypeSpecInfo(
+ typeId: TypeIds.int8,
+ nullable: nullable,
+ ref: ref,
+ dynamic: dynamic,
+ );
+ case 'Int16Type':
+ return _TypeSpecInfo(
+ typeId: TypeIds.int16,
+ nullable: nullable,
+ ref: ref,
+ dynamic: dynamic,
+ );
+ case 'Int32Type':
+ return _TypeSpecInfo(
+ typeId: _encodingTypeId(
+ reader,
+ fixed: TypeIds.int32,
+ varint: TypeIds.varInt32,
+ field: field,
+ ),
+ nullable: nullable,
+ ref: ref,
+ dynamic: dynamic,
+ );
+ case 'Int64Type':
+ return _TypeSpecInfo(
+ typeId: _encodingTypeId(
+ reader,
+ fixed: TypeIds.int64,
+ varint: TypeIds.varInt64,
+ tagged: TypeIds.taggedInt64,
+ field: field,
+ ),
+ nullable: nullable,
+ ref: ref,
+ dynamic: dynamic,
+ );
+ case 'Uint8Type':
+ return _TypeSpecInfo(
+ typeId: TypeIds.uint8,
+ nullable: nullable,
+ ref: ref,
+ dynamic: dynamic,
+ );
+ case 'Uint16Type':
+ return _TypeSpecInfo(
+ typeId: TypeIds.uint16,
+ nullable: nullable,
+ ref: ref,
+ dynamic: dynamic,
+ );
+ case 'Uint32Type':
+ return _TypeSpecInfo(
+ typeId: _encodingTypeId(
+ reader,
+ fixed: TypeIds.uint32,
+ varint: TypeIds.varUint32,
+ field: field,
+ ),
+ nullable: nullable,
+ ref: ref,
+ dynamic: dynamic,
+ );
+ case 'Uint64Type':
+ return _TypeSpecInfo(
+ typeId: _encodingTypeId(
+ reader,
+ fixed: TypeIds.uint64,
+ varint: TypeIds.varUint64,
+ tagged: TypeIds.taggedUint64,
+ field: field,
+ ),
+ nullable: nullable,
+ ref: ref,
+ dynamic: dynamic,
+ );
+ case 'Float16Type':
+ return _TypeSpecInfo(
+ typeId: TypeIds.float16,
+ nullable: nullable,
+ ref: ref,
+ dynamic: dynamic,
+ );
+ case 'Bfloat16Type':
+ return _TypeSpecInfo(
+ typeId: TypeIds.bfloat16,
+ nullable: nullable,
+ ref: ref,
+ dynamic: dynamic,
+ );
+ case 'Float32Type':
+ return _TypeSpecInfo(
+ typeId: TypeIds.float32,
+ nullable: nullable,
+ ref: ref,
+ dynamic: dynamic,
+ );
+ case 'Float64Type':
+ return _TypeSpecInfo(
+ typeId: TypeIds.float64,
+ nullable: nullable,
+ ref: ref,
+ dynamic: dynamic,
+ );
+ case 'StringType':
+ return _TypeSpecInfo(
+ typeId: TypeIds.string,
+ nullable: nullable,
+ ref: ref,
+ dynamic: dynamic,
+ );
+ case 'DecimalType':
+ return _TypeSpecInfo(
+ typeId: TypeIds.decimal,
+ nullable: nullable,
+ ref: ref,
+ dynamic: dynamic,
+ );
+ case 'TimestampType':
+ return _TypeSpecInfo(
+ typeId: TypeIds.timestamp,
+ nullable: nullable,
+ ref: ref,
+ dynamic: dynamic,
+ );
+ case 'DateType':
+ return _TypeSpecInfo(
+ typeId: TypeIds.date,
+ nullable: nullable,
+ ref: ref,
+ dynamic: dynamic,
+ );
+ case 'DurationType':
+ return _TypeSpecInfo(
+ typeId: TypeIds.duration,
+ nullable: nullable,
+ ref: ref,
+ dynamic: dynamic,
+ );
+ case 'BinaryType':
+ return _TypeSpecInfo(
+ typeId: TypeIds.binary,
+ nullable: nullable,
+ ref: ref,
+ dynamic: dynamic,
+ );
+ default:
+ throw InvalidGenerationSourceError(
+ 'Unsupported type spec ${typeName ?? reader.objectValue.toString()}.',
+ element: field,
+ );
}
- return null;
}
- _ListTypeSpecInfo _readListTypeSpec(ConstantReader reader) {
- final options = _readTypeOptions(reader);
- final elementObj = reader.peek('element');
- final element = elementObj != null && !elementObj.isNull
- ? _readTypeSpecObj(elementObj)
- : null;
- return _ListTypeSpecInfo(
- ref: options.ref,
- nullable: options.nullable,
- element: element,
- );
+ bool? _readBoolOverride(ConstantReader? reader) {
+ if (reader == null || reader.isNull) {
+ return null;
+ }
+ return reader.boolValue;
}
- _MapTypeSpecInfo _readMapTypeSpec(ConstantReader reader) {
- final options = _readTypeOptions(reader);
- final keyObj = reader.peek('key');
- final valueObj = reader.peek('value');
- final key =
- keyObj != null && !keyObj.isNull ? _readTypeSpecObj(keyObj) : null;
- final value = valueObj != null && !valueObj.isNull
- ? _readTypeSpecObj(valueObj)
- : null;
- return _MapTypeSpecInfo(
- ref: options.ref,
- nullable: options.nullable,
- key: key,
- value: value,
+ int _encodingTypeId(
+ ConstantReader reader, {
+ required int fixed,
+ required int varint,
+ int? tagged,
+ required Element field,
+ }) {
+ final encodingReader = reader.peek('encoding');
+ final encodingValue = encodingReader == null || encodingReader.isNull
+ ? 'varint'
+ : encodingReader.revive().accessor.split('.').last;
+ return switch (encodingValue) {
+ 'fixed' => fixed,
+ 'varint' => varint,
+ 'tagged' when tagged != null => tagged,
+ _ => throw InvalidGenerationSourceError(
+ 'Unsupported encoding $encodingValue for type spec.',
+ element: field,
+ ),
+ };
+ }
+
+ void _validateRootTypeSpecConflicts(
+ FieldElement field,
+ ConstantReader reader,
+ _TypeSpecInfo typeSpec,
+ ) {
+ final fieldNullable = reader.peek('nullable');
+ if (fieldNullable != null &&
+ !fieldNullable.isNull &&
+ typeSpec.nullable != null &&
+ fieldNullable.boolValue != typeSpec.nullable) {
+ throw InvalidGenerationSourceError(
+ 'Field nullable conflicts with root type nullable override.',
+ element: field,
+ );
+ }
+ final fieldRef = reader.peek('ref');
+ if (fieldRef != null &&
+ !fieldRef.isNull &&
+ typeSpec.ref != null &&
+ fieldRef.boolValue != typeSpec.ref) {
+ throw InvalidGenerationSourceError(
+ 'Field ref conflicts with root type ref override.',
+ element: field,
+ );
+ }
+ final fieldDynamic = reader.peek('dynamic');
+ if (fieldDynamic != null &&
+ !fieldDynamic.isNull &&
+ typeSpec.dynamic != null &&
+ fieldDynamic.boolValue != typeSpec.dynamic) {
+ throw InvalidGenerationSourceError(
+ 'Field dynamic conflicts with root type dynamic override.',
+ element: field,
+ );
+ }
+ }
+
+ bool _isDefaultBoolArrayCarrier(DartType type) {
+ if (!_isList(type) || _isNullable(type)) {
+ return false;
+ }
+ final argument = (type as InterfaceType).typeArguments.single;
+ return !_isNullable(argument) &&
+ _withoutNullability(argument).isDartCoreBool;
+ }
+
+ void _validatePackedListCarrier(
+ DartType listOrSetType,
+ _GeneratedFieldTypeSpec elementType,
+ Element errorElement,
+ ) {
+ if (!_isList(listOrSetType)) {
+ return;
+ }
+ if (elementType.nullable ||
+ elementType.ref ||
+ elementType.dynamic == true) {
+ return;
+ }
+ final suggestedCarrier = switch (elementType.typeId) {
+ TypeIds.int8 => 'Int8List',
+ TypeIds.int16 => 'Int16List',
+ TypeIds.int32 => 'Int32List',
+ TypeIds.int64 => 'Int64List',
+ TypeIds.uint8 => 'Uint8List',
+ TypeIds.uint16 => 'Uint16List',
+ TypeIds.uint32 => 'Uint32List',
+ TypeIds.uint64 => 'Uint64List',
+ TypeIds.float16 => 'Float16List',
+ TypeIds.bfloat16 => 'Bfloat16List',
+ TypeIds.float32 => 'Float32List',
+ TypeIds.float64 => 'Float64List',
+ _ => null,
+ };
+ if (suggestedCarrier == null) {
+ return;
+ }
+ throw InvalidGenerationSourceError(
+ 'Generic List fields with non-null fixed-width ${_typeSpecName(elementType.typeId)} '
+ 'elements must use $suggestedCarrier instead.',
+ element: errorElement,
);
}
- _TypeSpecInfo _readTypeSpecObj(ConstantReader reader) {
- final objType = reader.objectValue.type;
- if (objType != null && _listTypeChecker.isExactlyType(objType)) {
- return _readListTypeSpec(reader);
- }
- if (objType != null && _mapTypeChecker.isExactlyType(objType)) {
- return _readMapTypeSpec(reader);
- }
- // ValueType or fallback
- final options = _readTypeOptions(reader);
- return _TypeSpecInfo(ref: options.ref, nullable: options.nullable);
- }
-
- ({bool? ref, bool? nullable}) _readTypeOptions(ConstantReader reader) {
- bool? ref;
- bool? nullable;
- final optionsReader = reader.peek('options');
- if (optionsReader != null && !optionsReader.isNull) {
- for (final optionObj in optionsReader.listValue) {
- final optionReader = ConstantReader(optionObj);
- final optionType = optionObj.type;
- if (optionType != null && _refOptionChecker.isExactlyType(optionType)) {
- ref = optionReader.peek('tracked')?.boolValue ?? true;
- } else if (optionType != null &&
- _nullableOptionChecker.isExactlyType(optionType)) {
- nullable = optionReader.peek('value')?.boolValue ?? true;
- }
- }
+ void _validateScalarTypeOverride(
+ DartType type,
+ int typeId,
+ bool? dynamic,
+ _TypeSpecInfo? typeSpec,
+ Element errorElement,
+ ) {
+ final declaredOnlyOverride = typeSpec != null && typeSpec.typeId == null;
+ if (dynamic == true &&
+ (TypeIds.isPrimitive(typeId) || _isBuiltInTypeId(typeId))) {
+ throw InvalidGenerationSourceError(
+ 'dynamic: true is not valid for fixed scalar or built-in leaf type ${_typeSpecName(typeId)}.',
+ element: errorElement,
+ );
+ }
+ if (typeId == TypeIds.list ||
+ typeId == TypeIds.set ||
+ typeId == TypeIds.map) {
+ throw InvalidGenerationSourceError(
+ 'Type override ${_typeSpecName(typeId)} does not match the declared ${_typeCodeString(type)} carrier.',
+ element: errorElement,
+ );
+ }
+ final nonNullable = _withoutNullability(type);
+ final valid = switch (_typeLiteral(nonNullable)) {
+ 'bool' => typeId == TypeIds.boolType,
+ 'int' => _isSupportedIntTypeId(typeId),
+ 'double' => typeId == TypeIds.float32 || typeId == TypeIds.float64,
+ 'String' => typeId == TypeIds.string,
+ 'Int64' => _isSigned64TypeId(typeId),
+ 'Uint64' => _isUnsigned64TypeId(typeId),
+ 'Float16' => typeId == TypeIds.float16,
+ 'Bfloat16' => typeId == TypeIds.bfloat16,
+ 'Float32' => typeId == TypeIds.float32,
+ 'Decimal' => typeId == TypeIds.decimal,
+ 'Timestamp' || 'DateTime' => typeId == TypeIds.timestamp,
+ 'LocalDate' => typeId == TypeIds.date,
+ 'Duration' => typeId == TypeIds.duration,
+ 'Uint8List' => typeId == TypeIds.binary || typeId == TypeIds.uint8Array,
+ 'Int8List' => typeId == TypeIds.int8Array,
+ 'Int16List' => typeId == TypeIds.int16Array,
+ 'Int32List' => typeId == TypeIds.int32Array,
+ 'Int64List' => typeId == TypeIds.int64Array,
+ 'Uint16List' => typeId == TypeIds.uint16Array,
+ 'Uint32List' => typeId == TypeIds.uint32Array,
+ 'Uint64List' => typeId == TypeIds.uint64Array,
+ 'Float16List' => typeId == TypeIds.float16Array,
+ 'Bfloat16List' => typeId == TypeIds.bfloat16Array,
+ 'Float32List' => typeId == TypeIds.float32Array,
+ 'Float64List' => typeId == TypeIds.float64Array,
+ _ => typeSpec == null || declaredOnlyOverride,
+ };
+ if (!valid) {
+ throw InvalidGenerationSourceError(
+ 'Type override ${_typeSpecName(typeId)} is not valid for declared Dart type ${_typeCodeString(type)}.',
+ element: errorElement,
+ );
+ }
+ }
+
+ bool _isSupportedIntTypeId(int typeId) =>
+ typeId == TypeIds.int8 ||
+ typeId == TypeIds.int16 ||
+ typeId == TypeIds.int32 ||
+ typeId == TypeIds.varInt32 ||
+ _isSigned64TypeId(typeId) ||
+ typeId == TypeIds.uint8 ||
+ typeId == TypeIds.uint16 ||
+ typeId == TypeIds.uint32 ||
+ typeId == TypeIds.varUint32 ||
+ _isUnsigned64TypeId(typeId);
+
+ bool _isSigned64TypeId(int typeId) =>
+ typeId == TypeIds.int64 ||
+ typeId == TypeIds.varInt64 ||
+ typeId == TypeIds.taggedInt64;
+
+ bool _isUnsigned64TypeId(int typeId) =>
+ typeId == TypeIds.uint64 ||
+ typeId == TypeIds.varUint64 ||
+ typeId == TypeIds.taggedUint64;
+
+ String _checkedGeneratedScalarExpression(int typeId, String valueExpression) {
+ switch (typeId) {
+ case TypeIds.int8:
+ return 'generatedCheckedInt8($valueExpression)';
+ case TypeIds.int16:
+ return 'generatedCheckedInt16($valueExpression)';
+ case TypeIds.int32:
+ case TypeIds.varInt32:
+ return 'generatedCheckedInt32($valueExpression)';
+ case TypeIds.uint8:
+ return 'generatedCheckedUint8($valueExpression)';
+ case TypeIds.uint16:
+ return 'generatedCheckedUint16($valueExpression)';
+ case TypeIds.uint32:
+ case TypeIds.varUint32:
+ return 'generatedCheckedUint32($valueExpression)';
+ default:
+ return valueExpression;
}
- return (ref: ref, nullable: nullable);
}
- int _typeIdFor(DartType type, {_IntegerAnnotationSpec? integerAnnotation}) {
- if (integerAnnotation != null) {
- return integerAnnotation.typeId;
+ String _typeSpecName(int typeId) {
+ switch (typeId) {
+ case TypeIds.boolType:
+ return 'BoolType';
+ case TypeIds.int8:
+ return 'Int8Type';
+ case TypeIds.int16:
+ return 'Int16Type';
+ case TypeIds.int32:
+ return 'Int32Type(encoding: Encoding.fixed)';
+ case TypeIds.varInt32:
+ return 'Int32Type(encoding: Encoding.varint)';
+ case TypeIds.int64:
+ return 'Int64Type(encoding: Encoding.fixed)';
+ case TypeIds.varInt64:
+ return 'Int64Type(encoding: Encoding.varint)';
+ case TypeIds.taggedInt64:
+ return 'Int64Type(encoding: Encoding.tagged)';
+ case TypeIds.uint8:
+ return 'Uint8Type';
+ case TypeIds.uint16:
+ return 'Uint16Type';
+ case TypeIds.uint32:
+ return 'Uint32Type(encoding: Encoding.fixed)';
+ case TypeIds.varUint32:
+ return 'Uint32Type(encoding: Encoding.varint)';
+ case TypeIds.uint64:
+ return 'Uint64Type(encoding: Encoding.fixed)';
+ case TypeIds.varUint64:
+ return 'Uint64Type(encoding: Encoding.varint)';
+ case TypeIds.taggedUint64:
+ return 'Uint64Type(encoding: Encoding.tagged)';
+ case TypeIds.float16:
+ return 'Float16Type';
+ case TypeIds.bfloat16:
+ return 'Bfloat16Type';
+ case TypeIds.float32:
+ return 'Float32Type';
+ case TypeIds.float64:
+ return 'Float64Type';
+ case TypeIds.string:
+ return 'StringType';
+ case TypeIds.binary:
+ return 'BinaryType';
+ case TypeIds.decimal:
+ return 'DecimalType';
+ case TypeIds.timestamp:
+ return 'TimestampType';
+ case TypeIds.date:
+ return 'DateType';
+ case TypeIds.duration:
+ return 'DurationType';
+ case TypeIds.list:
+ return 'ListType';
+ case TypeIds.set:
+ return 'SetType';
+ case TypeIds.map:
+ return 'MapType';
+ default:
+ return 'type id $typeId';
}
+ }
+
+ int _typeIdFor(DartType type) {
final nonNullable = _withoutNullability(type);
if (nonNullable.isDartCoreBool) {
return TypeIds.boolType;
@@ -2226,18 +2630,6 @@ GeneratedFieldType(
}
final typeLiteral = _typeLiteral(nonNullable);
switch (typeLiteral) {
- case 'Int8':
- return TypeIds.int8;
- case 'Int16':
- return TypeIds.int16;
- case 'Int32':
- return TypeIds.varInt32;
- case 'Uint8':
- return TypeIds.uint8;
- case 'Uint16':
- return TypeIds.uint16;
- case 'Uint32':
- return TypeIds.varUint32;
case 'Uint64':
return TypeIds.varUint64;
case 'Int64':
@@ -2570,29 +2962,22 @@ final class _ConstructorArgumentSpec {
});
}
-final class _IntegerAnnotationSpec {
- final int typeId;
-
- const _IntegerAnnotationSpec({required this.typeId});
-}
-
-/// Parsed representation of a [TypeSpec] annotation hierarchy.
class _TypeSpecInfo {
- final bool? ref;
+ final int? typeId;
final bool? nullable;
-
- const _TypeSpecInfo({this.ref, this.nullable});
-}
-
-final class _ListTypeSpecInfo extends _TypeSpecInfo {
+ final bool? ref;
+ final bool? dynamic;
final _TypeSpecInfo? element;
-
- const _ListTypeSpecInfo({super.ref, super.nullable, this.element});
-}
-
-final class _MapTypeSpecInfo extends _TypeSpecInfo {
final _TypeSpecInfo? key;
final _TypeSpecInfo? value;
- const _MapTypeSpecInfo({super.ref, super.nullable, this.key, this.value});
+ const _TypeSpecInfo({
+ this.typeId,
+ this.nullable,
+ this.ref,
+ this.dynamic,
+ this.element,
+ this.key,
+ this.value,
+ });
}
diff --git a/dart/packages/fory/lib/src/codegen/generated_support.dart b/dart/packages/fory/lib/src/codegen/generated_support.dart
index d3ff7290d..c2cf44252 100644
--- a/dart/packages/fory/lib/src/codegen/generated_support.dart
+++ b/dart/packages/fory/lib/src/codegen/generated_support.dart
@@ -37,6 +37,7 @@ import 'package:fory/src/serializer/struct_serializer.dart';
import 'package:fory/src/serializer/struct_slots.dart';
import 'package:fory/src/serializer/time_serializers.dart';
import 'package:fory/src/serializer/typed_array_serializers.dart';
+import 'package:fory/src/util/int_validation.dart';
@internal
final class GeneratedFieldType {
@@ -247,6 +248,30 @@ Uint8List readGeneratedBinaryValue(ReadContext context) {
return BinarySerializer.readPayload(context);
}
+@internal
+@pragma('vm:prefer-inline')
+int generatedCheckedInt8(int value) => checkedInt8(value);
+
+@internal
+@pragma('vm:prefer-inline')
+int generatedCheckedInt16(int value) => checkedInt16(value);
+
+@internal
+@pragma('vm:prefer-inline')
+int generatedCheckedInt32(int value) => checkedInt32(value);
+
+@internal
+@pragma('vm:prefer-inline')
+int generatedCheckedUint8(int value) => checkedUint8(value);
+
+@internal
+@pragma('vm:prefer-inline')
+int generatedCheckedUint16(int value) => checkedUint16(value);
+
+@internal
+@pragma('vm:prefer-inline')
+int generatedCheckedUint32(int value) => checkedUint32(value);
+
@internal
void writeGeneratedBoolArrayValue(WriteContext context, List<bool> value) {
const BoolArraySerializer().write(context, value);
diff --git a/dart/packages/fory/lib/src/context/write_context.dart b/dart/packages/fory/lib/src/context/write_context.dart
index a9399818c..ee65523ba 100644
--- a/dart/packages/fory/lib/src/context/write_context.dart
+++ b/dart/packages/fory/lib/src/context/write_context.dart
@@ -220,6 +220,28 @@ final class WriteContext {
writeResolvedValue(resolved, value, null);
}
+ @internal
+ void writeRootBuiltinValue(
+ Object value, {
+ required int wireTypeId,
+ required bool trackRef,
+ }) {
+ final resolved = _typeResolver.resolveBuiltinWireType(wireTypeId);
+ final effectiveTrackRef = trackRef && resolved.supportsRef;
+ if (_refWriter.writeRefOrNull(
+ _buffer,
+ value,
+ trackRef: effectiveTrackRef,
+ )) {
+ return;
+ }
+ if (!effectiveTrackRef && resolved.supportsRef) {
+ _refWriter.reference(value);
+ }
+ _writeTypeMeta(resolved, value);
+ writeResolvedValue(resolved, value, null);
+ }
+
void _writeRef(Object? value, {required bool trackRef}) {
final resolved = value == null ? null : _typeResolver.resolveValue(value);
final effectiveTrackRef =
diff --git a/dart/packages/fory/lib/src/fory.dart b/dart/packages/fory/lib/src/fory.dart
index 72b2e64b9..f082a4eab 100644
--- a/dart/packages/fory/lib/src/fory.dart
+++ b/dart/packages/fory/lib/src/fory.dart
@@ -93,6 +93,27 @@ final class Fory {
return Uint8List.fromList(_buffer.toBytes());
}
+ /// Serializes a non-null builtin [value] using the explicit xlang
+ /// [wireTypeId].
+ ///
+ /// This is the advanced escape hatch for root values whose wire type must
+ /// stay narrower than their Dart carrier type, such as writing a Dart `int`
+ /// as xlang `VAR_INT32`. [wireTypeId] must identify a builtin xlang type.
+ Uint8List serializeBuiltin(
+ Object value, {
+ required int wireTypeId,
+ bool trackRef = false,
+ }) {
+ _buffer.clear();
+ serializeBuiltinTo(
+ value,
+ _buffer,
+ wireTypeId: wireTypeId,
+ trackRef: trackRef,
+ );
+ return Uint8List.fromList(_buffer.toBytes());
+ }
+
/// Serializes [value] into [buffer].
///
/// The target [buffer] is cleared before bytes are written. [trackRef] has
@@ -112,6 +133,31 @@ final class Fory {
}
}
+ /// Serializes a non-null builtin [value] into [buffer] using the explicit
+ /// xlang [wireTypeId].
+ ///
+ /// The target [buffer] is cleared before bytes are written. [wireTypeId]
+ /// must identify a builtin xlang type.
+ void serializeBuiltinTo(
+ Object value,
+ Buffer buffer, {
+ required int wireTypeId,
+ bool trackRef = false,
+ }) {
+ buffer.clear();
+ _writeContext.prepare(buffer, trackRef: trackRef);
+ try {
+ buffer.writeUint8(_xlangHeaderFlag);
+ _writeContext.writeRootBuiltinValue(
+ value,
+ wireTypeId: wireTypeId,
+ trackRef: trackRef,
+ );
+ } finally {
+ _writeContext.reset();
+ }
+ }
+
/// Deserializes [bytes] and then checks that the result is assignable to [T].
///
/// The payload is decoded from its wire metadata first. `T` is used as a
diff --git a/dart/packages/fory/lib/src/resolver/type_resolver.dart b/dart/packages/fory/lib/src/resolver/type_resolver.dart
index 8e826275f..b37562b22 100644
--- a/dart/packages/fory/lib/src/resolver/type_resolver.dart
+++ b/dart/packages/fory/lib/src/resolver/type_resolver.dart
@@ -46,16 +46,10 @@ import 'package:fory/src/types/bfloat16.dart';
import 'package:fory/src/types/decimal.dart';
import 'package:fory/src/types/float16.dart';
import 'package:fory/src/types/float32.dart';
-import 'package:fory/src/types/int16.dart';
-import 'package:fory/src/types/int32.dart';
import 'package:fory/src/types/int64.dart';
-import 'package:fory/src/types/int8.dart';
import 'package:fory/src/types/local_date.dart';
import 'package:fory/src/types/timestamp.dart';
-import 'package:fory/src/types/uint16.dart';
-import 'package:fory/src/types/uint32.dart';
import 'package:fory/src/types/uint64.dart';
-import 'package:fory/src/types/uint8.dart';
import 'package:fory/src/util/hash_util.dart';
enum RegistrationKind { builtin, struct, enumType, ext, union }
@@ -315,15 +309,6 @@ final class TypeResolver {
if (value is bool) {
return _builtin(bool, TypeIds.boolType);
}
- if (value is Int8) {
- return _builtin(Int8, TypeIds.int8);
- }
- if (value is Int16) {
- return _builtin(Int16, TypeIds.int16);
- }
- if (value is Int32) {
- return _builtin(Int32, TypeIds.varInt32);
- }
// Native Int64 is represented as `int`; web still needs this branch to
// keep explicit wrapper values on the Int64 API while using varint64 wire
// format by default, the same as plain Dart int.
@@ -334,15 +319,6 @@ final class TypeResolver {
if (value is int) {
return _builtin(int, TypeIds.varInt64);
}
- if (value is Uint8) {
- return _builtin(Uint8, TypeIds.uint8);
- }
- if (value is Uint16) {
- return _builtin(Uint16, TypeIds.uint16);
- }
- if (value is Uint32) {
- return _builtin(Uint32, TypeIds.varUint32);
- }
if (value is Uint64) {
return _builtin(Uint64, TypeIds.varUint64);
}
@@ -1030,13 +1006,13 @@ final class TypeResolver {
case TypeIds.boolType:
return _builtin(bool, TypeIds.boolType);
case TypeIds.int8:
- return _builtin(Int8, TypeIds.int8);
+ return _builtin(int, TypeIds.int8);
case TypeIds.int16:
- return _builtin(Int16, TypeIds.int16);
+ return _builtin(int, TypeIds.int16);
case TypeIds.int32:
- return _builtin(Int32, TypeIds.int32);
+ return _builtin(int, TypeIds.int32);
case TypeIds.varInt32:
- return _builtin(Int32, TypeIds.varInt32);
+ return _builtin(int, TypeIds.varInt32);
case TypeIds.int64:
return _builtin(Int64, TypeIds.int64);
case TypeIds.varInt64:
@@ -1044,13 +1020,13 @@ final class TypeResolver {
case TypeIds.taggedInt64:
return _builtin(Int64, TypeIds.taggedInt64);
case TypeIds.uint8:
- return _builtin(Uint8, TypeIds.uint8);
+ return _builtin(int, TypeIds.uint8);
case TypeIds.uint16:
- return _builtin(Uint16, TypeIds.uint16);
+ return _builtin(int, TypeIds.uint16);
case TypeIds.uint32:
- return _builtin(Uint32, TypeIds.uint32);
+ return _builtin(int, TypeIds.uint32);
case TypeIds.varUint32:
- return _builtin(Uint32, TypeIds.varUint32);
+ return _builtin(int, TypeIds.varUint32);
case TypeIds.uint64:
return _builtin(Uint64, TypeIds.uint64);
case TypeIds.varUint64:
@@ -1256,14 +1232,8 @@ final class TypeResolver {
if (type == bool) {
return TypeIds.boolType;
}
- if (type == Int8) {
- return TypeIds.int8;
- }
- if (type == Int16) {
- return TypeIds.int16;
- }
- if (type == Int32) {
- return TypeIds.varInt32;
+ if (type == int) {
+ return TypeIds.varInt64;
}
if (type == Int64) {
return TypeIds.varInt64;
@@ -1271,15 +1241,6 @@ final class TypeResolver {
if (type == Int64List) {
return TypeIds.int64Array;
}
- if (type == Uint8) {
- return TypeIds.uint8;
- }
- if (type == Uint16) {
- return TypeIds.uint16;
- }
- if (type == Uint32) {
- return TypeIds.varUint32;
- }
if (type == Uint64) {
return TypeIds.varUint64;
}
diff --git a/dart/packages/fory/lib/src/serializer/primitive_serializers.dart b/dart/packages/fory/lib/src/serializer/primitive_serializers.dart
index af8e249ee..31e1c1384 100644
--- a/dart/packages/fory/lib/src/serializer/primitive_serializers.dart
+++ b/dart/packages/fory/lib/src/serializer/primitive_serializers.dart
@@ -24,14 +24,9 @@ import 'package:fory/src/serializer/serializer.dart';
import 'package:fory/src/types/bfloat16.dart';
import 'package:fory/src/types/float16.dart';
import 'package:fory/src/types/float32.dart';
-import 'package:fory/src/types/int16.dart';
-import 'package:fory/src/types/int32.dart';
import 'package:fory/src/types/int64.dart';
-import 'package:fory/src/types/int8.dart';
-import 'package:fory/src/types/uint16.dart';
-import 'package:fory/src/types/uint32.dart';
import 'package:fory/src/types/uint64.dart';
-import 'package:fory/src/types/uint8.dart';
+import 'package:fory/src/util/int_validation.dart';
const int _jsSafeUint64IntMax = 9007199254740991;
const bool _isWeb = bool.fromEnvironment('dart.library.js_interop');
@@ -69,16 +64,16 @@ final class PrimitiveSerializer<T> extends Serializer<T> {
buffer.writeBool(value as bool);
return;
case TypeIds.int8:
- buffer.writeByte((value as Int8).value);
+ buffer.writeByte(checkedInt8(value as int));
return;
case TypeIds.int16:
- buffer.writeInt16((value as Int16).value);
+ buffer.writeInt16(checkedInt16(value as int));
return;
case TypeIds.int32:
- buffer.writeInt32((value as Int32).value);
+ buffer.writeInt32(checkedInt32(value as int));
return;
case TypeIds.varInt32:
- buffer.writeVarInt32(value is Int32 ? value.value : value as int);
+ buffer.writeVarInt32(checkedInt32(value as int));
return;
case TypeIds.int64:
if (value is Int64) {
@@ -102,16 +97,16 @@ final class PrimitiveSerializer<T> extends Serializer<T> {
}
return;
case TypeIds.uint8:
- buffer.writeUint8(value is Uint8 ? value.value : value as int);
+ buffer.writeUint8(checkedUint8(value as int));
return;
case TypeIds.uint16:
- buffer.writeUint16(value is Uint16 ? value.value : value as int);
+ buffer.writeUint16(checkedUint16(value as int));
return;
case TypeIds.uint32:
- buffer.writeUint32(value is Uint32 ? value.value : value as int);
+ buffer.writeUint32(checkedUint32(value as int));
return;
case TypeIds.varUint32:
- buffer.writeVarUint32(value is Uint32 ? value.value : value as int);
+ buffer.writeVarUint32(checkedUint32(value as int));
return;
case TypeIds.uint64:
buffer.writeUint64(_uint64Value(value));
@@ -148,13 +143,13 @@ final class PrimitiveSerializer<T> extends Serializer<T> {
case TypeIds.boolType:
return buffer.readBool();
case TypeIds.int8:
- return Int8(buffer.readByte());
+ return buffer.readByte();
case TypeIds.int16:
- return Int16(buffer.readInt16());
+ return buffer.readInt16();
case TypeIds.int32:
- return Int32(buffer.readInt32());
+ return buffer.readInt32();
case TypeIds.varInt32:
- return Int32(buffer.readVarInt32());
+ return buffer.readVarInt32();
case TypeIds.int64:
return buffer.readInt64();
case TypeIds.varInt64:
@@ -162,13 +157,13 @@ final class PrimitiveSerializer<T> extends Serializer<T> {
case TypeIds.taggedInt64:
return buffer.readTaggedInt64();
case TypeIds.uint8:
- return Uint8(buffer.readUint8());
+ return buffer.readUint8();
case TypeIds.uint16:
- return Uint16(buffer.readUint16());
+ return buffer.readUint16();
case TypeIds.uint32:
- return Uint32(buffer.readUint32());
+ return buffer.readUint32();
case TypeIds.varUint32:
- return Uint32(buffer.readVarUint32());
+ return buffer.readVarUint32();
case TypeIds.uint64:
return buffer.readUint64();
case TypeIds.varUint64:
@@ -208,20 +203,19 @@ const PrimitiveSerializer<bool> boolSerializer = PrimitiveSerializer<bool>(
TypeIds.boolType,
supportsRef: false,
);
-const PrimitiveSerializer<Int8> int8Serializer = PrimitiveSerializer<Int8>(
+const PrimitiveSerializer<int> int8Serializer = PrimitiveSerializer<int>(
TypeIds.int8,
supportsRef: false,
);
-const PrimitiveSerializer<Int16> int16Serializer = PrimitiveSerializer<Int16>(
+const PrimitiveSerializer<int> int16Serializer = PrimitiveSerializer<int>(
TypeIds.int16,
supportsRef: false,
);
-const PrimitiveSerializer<Int32> int32Serializer = PrimitiveSerializer<Int32>(
+const PrimitiveSerializer<int> int32Serializer = PrimitiveSerializer<int>(
TypeIds.int32,
supportsRef: false,
);
-const PrimitiveSerializer<Int32> varInt32Serializer =
- PrimitiveSerializer<Int32>(
+const PrimitiveSerializer<int> varInt32Serializer = PrimitiveSerializer<int>(
TypeIds.varInt32,
supportsRef: false,
);
@@ -239,22 +233,19 @@ const PrimitiveSerializer<Int64> taggedInt64Serializer =
TypeIds.taggedInt64,
supportsRef: false,
);
-const PrimitiveSerializer<Uint8> uint8Serializer = PrimitiveSerializer<Uint8>(
+const PrimitiveSerializer<int> uint8Serializer = PrimitiveSerializer<int>(
TypeIds.uint8,
supportsRef: false,
);
-const PrimitiveSerializer<Uint16> uint16Serializer =
- PrimitiveSerializer<Uint16>(
+const PrimitiveSerializer<int> uint16Serializer = PrimitiveSerializer<int>(
TypeIds.uint16,
supportsRef: false,
);
-const PrimitiveSerializer<Uint32> uint32Serializer =
- PrimitiveSerializer<Uint32>(
+const PrimitiveSerializer<int> uint32Serializer = PrimitiveSerializer<int>(
TypeIds.uint32,
supportsRef: false,
);
-const PrimitiveSerializer<Uint32> varUint32Serializer =
- PrimitiveSerializer<Uint32>(
+const PrimitiveSerializer<int> varUint32Serializer = PrimitiveSerializer<int>(
TypeIds.varUint32,
supportsRef: false,
);
diff --git a/dart/packages/fory/lib/src/serializer/serializer_support.dart b/dart/packages/fory/lib/src/serializer/serializer_support.dart
index 32b1dccd7..50a90b717 100644
--- a/dart/packages/fory/lib/src/serializer/serializer_support.dart
+++ b/dart/packages/fory/lib/src/serializer/serializer_support.dart
@@ -26,14 +26,8 @@ import 'package:fory/src/meta/type_ids.dart';
import 'package:fory/src/resolver/type_resolver.dart';
import 'package:fory/src/serializer/serialization_field_info.dart';
import 'package:fory/src/types/float32.dart';
-import 'package:fory/src/types/int16.dart';
-import 'package:fory/src/types/int32.dart';
import 'package:fory/src/types/int64.dart';
-import 'package:fory/src/types/int8.dart';
-import 'package:fory/src/types/uint16.dart';
-import 'package:fory/src/types/uint32.dart';
import 'package:fory/src/types/uint64.dart';
-import 'package:fory/src/types/uint8.dart';
final class DeferredReadRef {
final int id;
@@ -58,26 +52,12 @@ bool fieldUsesDeclaredType(
Object convertPrimitiveFieldValue(Object value, FieldType fieldType) {
if (fieldType.type == int) {
switch (fieldType.typeId) {
- case TypeIds.int8:
- return (value as Int8).value;
- case TypeIds.int16:
- return (value as Int16).value;
- case TypeIds.int32:
- case TypeIds.varInt32:
- return (value as Int32).value;
case TypeIds.int64:
case TypeIds.varInt64:
case TypeIds.taggedInt64:
return _declares64BitWrapper(fieldType)
? value
: (value as Int64).toInt();
- case TypeIds.uint8:
- return (value as Uint8).value;
- case TypeIds.uint16:
- return (value as Uint16).value;
- case TypeIds.uint32:
- case TypeIds.varUint32:
- return (value as Uint32).value;
case TypeIds.uint64:
case TypeIds.varUint64:
case TypeIds.taggedUint64:
@@ -122,24 +102,10 @@ Object convertResolvedPrimitiveValue(
}
if (resolved.type == int) {
switch (resolved.typeId) {
- case TypeIds.int8:
- return (value as Int8).value;
- case TypeIds.int16:
- return (value as Int16).value;
- case TypeIds.int32:
- case TypeIds.varInt32:
- return (value as Int32).value;
case TypeIds.int64:
case TypeIds.varInt64:
case TypeIds.taggedInt64:
return (value as Int64).toInt();
- case TypeIds.uint8:
- return (value as Uint8).value;
- case TypeIds.uint16:
- return (value as Uint16).value;
- case TypeIds.uint32:
- case TypeIds.varUint32:
- return (value as Uint32).value;
case TypeIds.uint64:
case TypeIds.varUint64:
case TypeIds.taggedUint64:
diff --git a/dart/packages/fory/lib/src/types/int16.dart b/dart/packages/fory/lib/src/types/int16.dart
deleted file mode 100644
index ddef75b88..000000000
--- a/dart/packages/fory/lib/src/types/int16.dart
+++ /dev/null
@@ -1,145 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-/// Signed 16-bit integer wrapper used by the xlang type system.
-///
-/// The constructor truncates [value] to 16 bits using two's-complement
-/// semantics, and arithmetic operators normalize every result back to the
-/// signed 16-bit range.
-final class Int16 implements Comparable<Int16> {
- /// The normalized signed 16-bit value.
- final int value;
-
- /// Creates a signed 16-bit value by truncating [value] to 16 bits.
- Int16(int value) : value = value.toSigned(16);
-
- @override
- int compareTo(Int16 other) => value.compareTo(other.value);
-
- Int16 operator +(Object other) => switch (other) {
- int otherValue => Int16(value + otherValue),
- Int16 otherValue => Int16(value + otherValue.value),
- _ => throw ArgumentError.value(
- other, 'other', 'Expected an int or Int16.'),
- };
-
- Int16 operator -(Object other) => switch (other) {
- int otherValue => Int16(value - otherValue),
- Int16 otherValue => Int16(value - otherValue.value),
- _ => throw ArgumentError.value(
- other, 'other', 'Expected an int or Int16.'),
- };
-
- Int16 operator *(Object other) => switch (other) {
- int otherValue => Int16(value * otherValue),
- Int16 otherValue => Int16(value * otherValue.value),
- _ => throw ArgumentError.value(
- other, 'other', 'Expected an int or Int16.'),
- };
-
- Int16 operator ~/(Object other) => switch (other) {
- int otherValue => Int16(value ~/ otherValue),
- Int16 otherValue => Int16(value ~/ otherValue.value),
- _ => throw ArgumentError.value(
- other, 'other', 'Expected an int or Int16.'),
- };
-
- Int16 operator %(Object other) => switch (other) {
- int otherValue => Int16(value % otherValue),
- Int16 otherValue => Int16(value % otherValue.value),
- _ => throw ArgumentError.value(
- other, 'other', 'Expected an int or Int16.'),
- };
-
- double operator /(Object other) => switch (other) {
- int otherValue => value / otherValue,
- Int16 otherValue => value / otherValue.value,
- _ => throw ArgumentError.value(
- other, 'other', 'Expected an int or Int16.'),
- };
-
- Int16 operator -() => Int16(-value);
-
- Int16 operator ~() => Int16(~value);
-
- Int16 operator &(Object other) => switch (other) {
- int otherValue => Int16(value & otherValue),
- Int16 otherValue => Int16(value & otherValue.value),
- _ => throw ArgumentError.value(
- other, 'other', 'Expected an int or Int16.'),
- };
-
- Int16 operator |(Object other) => switch (other) {
- int otherValue => Int16(value | otherValue),
- Int16 otherValue => Int16(value | otherValue.value),
- _ => throw ArgumentError.value(
- other, 'other', 'Expected an int or Int16.'),
- };
-
- Int16 operator ^(Object other) => switch (other) {
- int otherValue => Int16(value ^ otherValue),
- Int16 otherValue => Int16(value ^ otherValue.value),
- _ => throw ArgumentError.value(
- other, 'other', 'Expected an int or Int16.'),
- };
-
- Int16 operator <<(int shift) => Int16(value << shift);
-
- Int16 operator >>(int shift) => Int16(value >> shift);
-
- bool operator <(Object other) => switch (other) {
- int otherValue => value < otherValue,
- Int16 otherValue => value < otherValue.value,
- _ => throw ArgumentError.value(
- other, 'other', 'Expected an int or Int16.'),
- };
-
- bool operator <=(Object other) => switch (other) {
- int otherValue => value <= otherValue,
- Int16 otherValue => value <= otherValue.value,
- _ => throw ArgumentError.value(
- other, 'other', 'Expected an int or Int16.'),
- };
-
- bool operator >(Object other) => switch (other) {
- int otherValue => value > otherValue,
- Int16 otherValue => value > otherValue.value,
- _ => throw ArgumentError.value(
- other, 'other', 'Expected an int or Int16.'),
- };
-
- bool operator >=(Object other) => switch (other) {
- int otherValue => value >= otherValue,
- Int16 otherValue => value >= otherValue.value,
- _ => throw ArgumentError.value(
- other, 'other', 'Expected an int or Int16.'),
- };
-
- int toInt() => value;
-
- @override
- bool operator ==(Object other) =>
- identical(this, other) || other is Int16 && other.value == value;
-
- @override
- int get hashCode => value.hashCode;
-
- @override
- String toString() => value.toString();
-}
diff --git a/dart/packages/fory/lib/src/types/int32.dart b/dart/packages/fory/lib/src/types/int32.dart
deleted file mode 100644
index 4bfa350f1..000000000
--- a/dart/packages/fory/lib/src/types/int32.dart
+++ /dev/null
@@ -1,145 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-/// Signed 32-bit integer wrapper used by the xlang type system.
-///
-/// This wrapper preserves xlang `int32` and `varint32` semantics on the Dart
-/// side. Arithmetic and bitwise operators normalize every result back to the
-/// signed 32-bit range.
-final class Int32 implements Comparable<Int32> {
- /// The normalized signed 32-bit value.
- final int value;
-
- /// Creates a signed 32-bit value by truncating [value] to 32 bits.
- Int32(int value) : value = value.toSigned(32);
-
- @override
- int compareTo(Int32 other) => value.compareTo(other.value);
-
- Int32 operator +(Object other) => switch (other) {
- int otherValue => Int32(value + otherValue),
- Int32 otherValue => Int32(value + otherValue.value),
- _ => throw ArgumentError.value(
- other, 'other', 'Expected an int or Int32.'),
- };
-
- Int32 operator -(Object other) => switch (other) {
- int otherValue => Int32(value - otherValue),
- Int32 otherValue => Int32(value - otherValue.value),
- _ => throw ArgumentError.value(
- other, 'other', 'Expected an int or Int32.'),
- };
-
- Int32 operator *(Object other) => switch (other) {
- int otherValue => Int32(value * otherValue),
- Int32 otherValue => Int32(value * otherValue.value),
- _ => throw ArgumentError.value(
- other, 'other', 'Expected an int or Int32.'),
- };
-
- Int32 operator ~/(Object other) => switch (other) {
- int otherValue => Int32(value ~/ otherValue),
- Int32 otherValue => Int32(value ~/ otherValue.value),
- _ => throw ArgumentError.value(
- other, 'other', 'Expected an int or Int32.'),
- };
-
- Int32 operator %(Object other) => switch (other) {
- int otherValue => Int32(value % otherValue),
- Int32 otherValue => Int32(value % otherValue.value),
- _ => throw ArgumentError.value(
- other, 'other', 'Expected an int or Int32.'),
- };
-
- double operator /(Object other) => switch (other) {
- int otherValue => value / otherValue,
- Int32 otherValue => value / otherValue.value,
- _ => throw ArgumentError.value(
- other, 'other', 'Expected an int or Int32.'),
- };
-
- Int32 operator -() => Int32(-value);
-
- Int32 operator ~() => Int32(~value);
-
- Int32 operator &(Object other) => switch (other) {
- int otherValue => Int32(value & otherValue),
- Int32 otherValue => Int32(value & otherValue.value),
- _ => throw ArgumentError.value(
- other, 'other', 'Expected an int or Int32.'),
- };
-
- Int32 operator |(Object other) => switch (other) {
- int otherValue => Int32(value | otherValue),
- Int32 otherValue => Int32(value | otherValue.value),
- _ => throw ArgumentError.value(
- other, 'other', 'Expected an int or Int32.'),
- };
-
- Int32 operator ^(Object other) => switch (other) {
- int otherValue => Int32(value ^ otherValue),
- Int32 otherValue => Int32(value ^ otherValue.value),
- _ => throw ArgumentError.value(
- other, 'other', 'Expected an int or Int32.'),
- };
-
- Int32 operator <<(int shift) => Int32(value << shift);
-
- Int32 operator >>(int shift) => Int32(value >> shift);
-
- bool operator <(Object other) => switch (other) {
- int otherValue => value < otherValue,
- Int32 otherValue => value < otherValue.value,
- _ => throw ArgumentError.value(
- other, 'other', 'Expected an int or Int32.'),
- };
-
- bool operator <=(Object other) => switch (other) {
- int otherValue => value <= otherValue,
- Int32 otherValue => value <= otherValue.value,
- _ => throw ArgumentError.value(
- other, 'other', 'Expected an int or Int32.'),
- };
-
- bool operator >(Object other) => switch (other) {
- int otherValue => value > otherValue,
- Int32 otherValue => value > otherValue.value,
- _ => throw ArgumentError.value(
- other, 'other', 'Expected an int or Int32.'),
- };
-
- bool operator >=(Object other) => switch (other) {
- int otherValue => value >= otherValue,
- Int32 otherValue => value >= otherValue.value,
- _ => throw ArgumentError.value(
- other, 'other', 'Expected an int or Int32.'),
- };
-
- int toInt() => value;
-
- @override
- bool operator ==(Object other) =>
- identical(this, other) || other is Int32 && other.value == value;
-
- @override
- int get hashCode => value.hashCode;
-
- @override
- String toString() => value.toString();
-}
diff --git a/dart/packages/fory/lib/src/types/int8.dart b/dart/packages/fory/lib/src/types/int8.dart
deleted file mode 100644
index 3d9531e86..000000000
--- a/dart/packages/fory/lib/src/types/int8.dart
+++ /dev/null
@@ -1,160 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-/// Signed 8-bit integer wrapper used by the xlang type system.
-///
-/// The constructor truncates [value] to 8 bits using two's-complement
-/// semantics, so arithmetic, shifts, and bitwise operators behave like native
-/// 8-bit machine operations. Operators that produce wrapped integers return a
-/// new normalized [Int8]; `/` returns a Dart [double].
-final class Int8 implements Comparable<Int8> {
- /// The normalized signed 8-bit value.
- final int value;
-
- /// Creates a signed 8-bit value by truncating [value] to 8 bits.
- Int8(int value) : value = value.toSigned(8);
-
- @override
- int compareTo(Int8 other) => value.compareTo(other.value);
-
- /// Returns a normalized sum.
- Int8 operator +(Object other) => switch (other) {
- int otherValue => Int8(value + otherValue),
- Int8 otherValue => Int8(value + otherValue.value),
- _ =>
- throw ArgumentError.value(other, 'other', 'Expected an int or Int8.'),
- };
-
- /// Returns a normalized difference.
- Int8 operator -(Object other) => switch (other) {
- int otherValue => Int8(value - otherValue),
- Int8 otherValue => Int8(value - otherValue.value),
- _ =>
- throw ArgumentError.value(other, 'other', 'Expected an int or Int8.'),
- };
-
- /// Returns a normalized product.
- Int8 operator *(Object other) => switch (other) {
- int otherValue => Int8(value * otherValue),
- Int8 otherValue => Int8(value * otherValue.value),
- _ =>
- throw ArgumentError.value(other, 'other', 'Expected an int or Int8.'),
- };
-
- /// Returns a normalized integer quotient.
- Int8 operator ~/(Object other) => switch (other) {
- int otherValue => Int8(value ~/ otherValue),
- Int8 otherValue => Int8(value ~/ otherValue.value),
- _ =>
- throw ArgumentError.value(other, 'other', 'Expected an int or Int8.'),
- };
-
- /// Returns a normalized remainder.
- Int8 operator %(Object other) => switch (other) {
- int otherValue => Int8(value % otherValue),
- Int8 otherValue => Int8(value % otherValue.value),
- _ =>
- throw ArgumentError.value(other, 'other', 'Expected an int or Int8.'),
- };
-
- /// Returns a Dart [double] quotient.
- double operator /(Object other) => switch (other) {
- int otherValue => value / otherValue,
- Int8 otherValue => value / otherValue.value,
- _ =>
- throw ArgumentError.value(other, 'other', 'Expected an int or Int8.'),
- };
-
- /// Returns the normalized negated value.
- Int8 operator -() => Int8(-value);
-
- /// Returns the normalized bitwise inverse.
- Int8 operator ~() => Int8(~value);
-
- /// Returns a normalized bitwise AND.
- Int8 operator &(Object other) => switch (other) {
- int otherValue => Int8(value & otherValue),
- Int8 otherValue => Int8(value & otherValue.value),
- _ =>
- throw ArgumentError.value(other, 'other', 'Expected an int or Int8.'),
- };
-
- /// Returns a normalized bitwise OR.
- Int8 operator |(Object other) => switch (other) {
- int otherValue => Int8(value | otherValue),
- Int8 otherValue => Int8(value | otherValue.value),
- _ =>
- throw ArgumentError.value(other, 'other', 'Expected an int or Int8.'),
- };
-
- /// Returns a normalized bitwise XOR.
- Int8 operator ^(Object other) => switch (other) {
- int otherValue => Int8(value ^ otherValue),
- Int8 otherValue => Int8(value ^ otherValue.value),
- _ =>
- throw ArgumentError.value(other, 'other', 'Expected an int or Int8.'),
- };
-
- /// Returns a normalized left shift.
- Int8 operator <<(int shift) => Int8(value << shift);
-
- /// Returns a normalized arithmetic right shift.
- Int8 operator >>(int shift) => Int8(value >> shift);
-
- bool operator <(Object other) => switch (other) {
- int otherValue => value < otherValue,
- Int8 otherValue => value < otherValue.value,
- _ =>
- throw ArgumentError.value(other, 'other', 'Expected an int or Int8.'),
- };
-
- bool operator <=(Object other) => switch (other) {
- int otherValue => value <= otherValue,
- Int8 otherValue => value <= otherValue.value,
- _ =>
- throw ArgumentError.value(other, 'other', 'Expected an int or Int8.'),
- };
-
- bool operator >(Object other) => switch (other) {
- int otherValue => value > otherValue,
- Int8 otherValue => value > otherValue.value,
- _ =>
- throw ArgumentError.value(other, 'other', 'Expected an int or Int8.'),
- };
-
- bool operator >=(Object other) => switch (other) {
- int otherValue => value >= otherValue,
- Int8 otherValue => value >= otherValue.value,
- _ =>
- throw ArgumentError.value(other, 'other', 'Expected an int or Int8.'),
- };
-
- /// Returns the normalized integer value as a Dart [int].
- int toInt() => value;
-
- @override
- bool operator ==(Object other) =>
- identical(this, other) || other is Int8 && other.value == value;
-
- @override
- int get hashCode => value.hashCode;
-
- @override
- String toString() => value.toString();
-}
diff --git a/dart/packages/fory/lib/src/types/uint16.dart b/dart/packages/fory/lib/src/types/uint16.dart
deleted file mode 100644
index 47bd69425..000000000
--- a/dart/packages/fory/lib/src/types/uint16.dart
+++ /dev/null
@@ -1,147 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-/// Unsigned 16-bit integer wrapper used by the xlang type system.
-///
-/// The constructor truncates [value] to 16 bits using unsigned wrap-around
-/// semantics. Arithmetic and bitwise operators normalize their results back to
-/// the `0..65535` range.
-final class Uint16 implements Comparable<Uint16> {
- /// The normalized unsigned 16-bit value.
- final int value;
-
- /// Creates an unsigned 16-bit value by truncating [value] to 16 bits.
- Uint16(int value) : value = value.toUnsigned(16);
-
- @override
- int compareTo(Uint16 other) => value.compareTo(other.value);
-
- Uint16 operator +(Object other) => switch (other) {
- int otherValue => Uint16(value + otherValue),
- Uint16 otherValue => Uint16(value + otherValue.value),
- _ => throw ArgumentError.value(
- other, 'other', 'Expected an int or Uint16.'),
- };
-
- Uint16 operator -(Object other) => switch (other) {
- int otherValue => Uint16(value - otherValue),
- Uint16 otherValue => Uint16(value - otherValue.value),
- _ => throw ArgumentError.value(
- other, 'other', 'Expected an int or Uint16.'),
- };
-
- Uint16 operator *(Object other) => switch (other) {
- int otherValue => Uint16(value * otherValue),
- Uint16 otherValue => Uint16(value * otherValue.value),
- _ => throw ArgumentError.value(
- other, 'other', 'Expected an int or Uint16.'),
- };
-
- Uint16 operator ~/(Object other) => switch (other) {
- int otherValue => Uint16(value ~/ otherValue),
- Uint16 otherValue => Uint16(value ~/ otherValue.value),
- _ => throw ArgumentError.value(
- other, 'other', 'Expected an int or Uint16.'),
- };
-
- Uint16 operator %(Object other) => switch (other) {
- int otherValue => Uint16(value % otherValue),
- Uint16 otherValue => Uint16(value % otherValue.value),
- _ => throw ArgumentError.value(
- other, 'other', 'Expected an int or Uint16.'),
- };
-
- double operator /(Object other) => switch (other) {
- int otherValue => value / otherValue,
- Uint16 otherValue => value / otherValue.value,
- _ => throw ArgumentError.value(
- other, 'other', 'Expected an int or Uint16.'),
- };
-
- Uint16 operator -() => Uint16(-value);
-
- Uint16 operator ~() => Uint16(~value);
-
- Uint16 operator &(Object other) => switch (other) {
- int otherValue => Uint16(value & otherValue),
- Uint16 otherValue => Uint16(value & otherValue.value),
- _ => throw ArgumentError.value(
- other, 'other', 'Expected an int or Uint16.'),
- };
-
- Uint16 operator |(Object other) => switch (other) {
- int otherValue => Uint16(value | otherValue),
- Uint16 otherValue => Uint16(value | otherValue.value),
- _ => throw ArgumentError.value(
- other, 'other', 'Expected an int or Uint16.'),
- };
-
- Uint16 operator ^(Object other) => switch (other) {
- int otherValue => Uint16(value ^ otherValue),
- Uint16 otherValue => Uint16(value ^ otherValue.value),
- _ => throw ArgumentError.value(
- other, 'other', 'Expected an int or Uint16.'),
- };
-
- Uint16 operator <<(int shift) => Uint16(value << shift);
-
- Uint16 operator >>(int shift) => Uint16(value >>> shift);
-
- Uint16 operator >>>(int shift) => Uint16(value >>> shift);
-
- bool operator <(Object other) => switch (other) {
- int otherValue => value < otherValue,
- Uint16 otherValue => value < otherValue.value,
- _ => throw ArgumentError.value(
- other, 'other', 'Expected an int or Uint16.'),
- };
-
- bool operator <=(Object other) => switch (other) {
- int otherValue => value <= otherValue,
- Uint16 otherValue => value <= otherValue.value,
- _ => throw ArgumentError.value(
- other, 'other', 'Expected an int or Uint16.'),
- };
-
- bool operator >(Object other) => switch (other) {
- int otherValue => value > otherValue,
- Uint16 otherValue => value > otherValue.value,
- _ => throw ArgumentError.value(
- other, 'other', 'Expected an int or Uint16.'),
- };
-
- bool operator >=(Object other) => switch (other) {
- int otherValue => value >= otherValue,
- Uint16 otherValue => value >= otherValue.value,
- _ => throw ArgumentError.value(
- other, 'other', 'Expected an int or Uint16.'),
- };
-
- int toInt() => value;
-
- @override
- bool operator ==(Object other) =>
- identical(this, other) || other is Uint16 && other.value == value;
-
- @override
- int get hashCode => value.hashCode;
-
- @override
- String toString() => value.toString();
-}
diff --git a/dart/packages/fory/lib/src/types/uint32.dart b/dart/packages/fory/lib/src/types/uint32.dart
deleted file mode 100644
index 7da4f5f31..000000000
--- a/dart/packages/fory/lib/src/types/uint32.dart
+++ /dev/null
@@ -1,147 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-/// Unsigned 32-bit integer wrapper used by the xlang type system.
-///
-/// This wrapper preserves xlang `uint32` and `varuint32` semantics on the Dart
-/// side. Arithmetic and bitwise operators normalize every result back to the
-/// unsigned 32-bit range.
-final class Uint32 implements Comparable<Uint32> {
- /// The normalized unsigned 32-bit value.
- final int value;
-
- /// Creates an unsigned 32-bit value by truncating [value] to 32 bits.
- Uint32(int value) : value = value.toUnsigned(32);
-
- @override
- int compareTo(Uint32 other) => value.compareTo(other.value);
-
- Uint32 operator +(Object other) => switch (other) {
- int otherValue => Uint32(value + otherValue),
- Uint32 otherValue => Uint32(value + otherValue.value),
- _ => throw ArgumentError.value(
- other, 'other', 'Expected an int or Uint32.'),
- };
-
- Uint32 operator -(Object other) => switch (other) {
- int otherValue => Uint32(value - otherValue),
- Uint32 otherValue => Uint32(value - otherValue.value),
- _ => throw ArgumentError.value(
- other, 'other', 'Expected an int or Uint32.'),
- };
-
- Uint32 operator *(Object other) => switch (other) {
- int otherValue => Uint32(value * otherValue),
- Uint32 otherValue => Uint32(value * otherValue.value),
- _ => throw ArgumentError.value(
- other, 'other', 'Expected an int or Uint32.'),
- };
-
- Uint32 operator ~/(Object other) => switch (other) {
- int otherValue => Uint32(value ~/ otherValue),
- Uint32 otherValue => Uint32(value ~/ otherValue.value),
- _ => throw ArgumentError.value(
- other, 'other', 'Expected an int or Uint32.'),
- };
-
- Uint32 operator %(Object other) => switch (other) {
- int otherValue => Uint32(value % otherValue),
- Uint32 otherValue => Uint32(value % otherValue.value),
- _ => throw ArgumentError.value(
- other, 'other', 'Expected an int or Uint32.'),
- };
-
- double operator /(Object other) => switch (other) {
- int otherValue => value / otherValue,
- Uint32 otherValue => value / otherValue.value,
- _ => throw ArgumentError.value(
- other, 'other', 'Expected an int or Uint32.'),
- };
-
- Uint32 operator -() => Uint32(-value);
-
- Uint32 operator ~() => Uint32(~value);
-
- Uint32 operator &(Object other) => switch (other) {
- int otherValue => Uint32(value & otherValue),
- Uint32 otherValue => Uint32(value & otherValue.value),
- _ => throw ArgumentError.value(
- other, 'other', 'Expected an int or Uint32.'),
- };
-
- Uint32 operator |(Object other) => switch (other) {
- int otherValue => Uint32(value | otherValue),
- Uint32 otherValue => Uint32(value | otherValue.value),
- _ => throw ArgumentError.value(
- other, 'other', 'Expected an int or Uint32.'),
- };
-
- Uint32 operator ^(Object other) => switch (other) {
- int otherValue => Uint32(value ^ otherValue),
- Uint32 otherValue => Uint32(value ^ otherValue.value),
- _ => throw ArgumentError.value(
- other, 'other', 'Expected an int or Uint32.'),
- };
-
- Uint32 operator <<(int shift) => Uint32(value << shift);
-
- Uint32 operator >>(int shift) => Uint32(value >>> shift);
-
- Uint32 operator >>>(int shift) => Uint32(value >>> shift);
-
- bool operator <(Object other) => switch (other) {
- int otherValue => value < otherValue,
- Uint32 otherValue => value < otherValue.value,
- _ => throw ArgumentError.value(
- other, 'other', 'Expected an int or Uint32.'),
- };
-
- bool operator <=(Object other) => switch (other) {
- int otherValue => value <= otherValue,
- Uint32 otherValue => value <= otherValue.value,
- _ => throw ArgumentError.value(
- other, 'other', 'Expected an int or Uint32.'),
- };
-
- bool operator >(Object other) => switch (other) {
- int otherValue => value > otherValue,
- Uint32 otherValue => value > otherValue.value,
- _ => throw ArgumentError.value(
- other, 'other', 'Expected an int or Uint32.'),
- };
-
- bool operator >=(Object other) => switch (other) {
- int otherValue => value >= otherValue,
- Uint32 otherValue => value >= otherValue.value,
- _ => throw ArgumentError.value(
- other, 'other', 'Expected an int or Uint32.'),
- };
-
- int toInt() => value;
-
- @override
- bool operator ==(Object other) =>
- identical(this, other) || other is Uint32 && other.value == value;
-
- @override
- int get hashCode => value.hashCode;
-
- @override
- String toString() => value.toString();
-}
diff --git a/dart/packages/fory/lib/src/types/uint8.dart b/dart/packages/fory/lib/src/types/uint8.dart
deleted file mode 100644
index bde29e661..000000000
--- a/dart/packages/fory/lib/src/types/uint8.dart
+++ /dev/null
@@ -1,147 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-/// Unsigned 8-bit integer wrapper used by the xlang type system.
-///
-/// The constructor truncates [value] to 8 bits using unsigned wrap-around
-/// semantics. Arithmetic and bitwise operators normalize their results back to
-/// the `0..255` range.
-final class Uint8 implements Comparable<Uint8> {
- /// The normalized unsigned 8-bit value.
- final int value;
-
- /// Creates an unsigned 8-bit value by truncating [value] to 8 bits.
- Uint8(int value) : value = value.toUnsigned(8);
-
- @override
- int compareTo(Uint8 other) => value.compareTo(other.value);
-
- Uint8 operator +(Object other) => switch (other) {
- int otherValue => Uint8(value + otherValue),
- Uint8 otherValue => Uint8(value + otherValue.value),
- _ => throw ArgumentError.value(
- other, 'other', 'Expected an int or Uint8.'),
- };
-
- Uint8 operator -(Object other) => switch (other) {
- int otherValue => Uint8(value - otherValue),
- Uint8 otherValue => Uint8(value - otherValue.value),
- _ => throw ArgumentError.value(
- other, 'other', 'Expected an int or Uint8.'),
- };
-
- Uint8 operator *(Object other) => switch (other) {
- int otherValue => Uint8(value * otherValue),
- Uint8 otherValue => Uint8(value * otherValue.value),
- _ => throw ArgumentError.value(
- other, 'other', 'Expected an int or Uint8.'),
- };
-
- Uint8 operator ~/(Object other) => switch (other) {
- int otherValue => Uint8(value ~/ otherValue),
- Uint8 otherValue => Uint8(value ~/ otherValue.value),
- _ => throw ArgumentError.value(
- other, 'other', 'Expected an int or Uint8.'),
- };
-
- Uint8 operator %(Object other) => switch (other) {
- int otherValue => Uint8(value % otherValue),
- Uint8 otherValue => Uint8(value % otherValue.value),
- _ => throw ArgumentError.value(
- other, 'other', 'Expected an int or Uint8.'),
- };
-
- double operator /(Object other) => switch (other) {
- int otherValue => value / otherValue,
- Uint8 otherValue => value / otherValue.value,
- _ => throw ArgumentError.value(
- other, 'other', 'Expected an int or Uint8.'),
- };
-
- Uint8 operator -() => Uint8(-value);
-
- Uint8 operator ~() => Uint8(~value);
-
- Uint8 operator &(Object other) => switch (other) {
- int otherValue => Uint8(value & otherValue),
- Uint8 otherValue => Uint8(value & otherValue.value),
- _ => throw ArgumentError.value(
- other, 'other', 'Expected an int or Uint8.'),
- };
-
- Uint8 operator |(Object other) => switch (other) {
- int otherValue => Uint8(value | otherValue),
- Uint8 otherValue => Uint8(value | otherValue.value),
- _ => throw ArgumentError.value(
- other, 'other', 'Expected an int or Uint8.'),
- };
-
- Uint8 operator ^(Object other) => switch (other) {
- int otherValue => Uint8(value ^ otherValue),
- Uint8 otherValue => Uint8(value ^ otherValue.value),
- _ => throw ArgumentError.value(
- other, 'other', 'Expected an int or Uint8.'),
- };
-
- Uint8 operator <<(int shift) => Uint8(value << shift);
-
- Uint8 operator >>(int shift) => Uint8(value >>> shift);
-
- Uint8 operator >>>(int shift) => Uint8(value >>> shift);
-
- bool operator <(Object other) => switch (other) {
- int otherValue => value < otherValue,
- Uint8 otherValue => value < otherValue.value,
- _ => throw ArgumentError.value(
- other, 'other', 'Expected an int or Uint8.'),
- };
-
- bool operator <=(Object other) => switch (other) {
- int otherValue => value <= otherValue,
- Uint8 otherValue => value <= otherValue.value,
- _ => throw ArgumentError.value(
- other, 'other', 'Expected an int or Uint8.'),
- };
-
- bool operator >(Object other) => switch (other) {
- int otherValue => value > otherValue,
- Uint8 otherValue => value > otherValue.value,
- _ => throw ArgumentError.value(
- other, 'other', 'Expected an int or Uint8.'),
- };
-
- bool operator >=(Object other) => switch (other) {
- int otherValue => value >= otherValue,
- Uint8 otherValue => value >= otherValue.value,
- _ => throw ArgumentError.value(
- other, 'other', 'Expected an int or Uint8.'),
- };
-
- int toInt() => value;
-
- @override
- bool operator ==(Object other) =>
- identical(this, other) || other is Uint8 && other.value == value;
-
- @override
- int get hashCode => value.hashCode;
-
- @override
- String toString() => value.toString();
-}
diff --git a/dart/packages/fory/lib/src/util/hash_util.dart b/dart/packages/fory/lib/src/util/hash_util.dart
index c49cdd018..0af369bca 100644
--- a/dart/packages/fory/lib/src/util/hash_util.dart
+++ b/dart/packages/fory/lib/src/util/hash_util.dart
@@ -19,7 +19,7 @@
import 'dart:convert';
-import 'package:fory/src/meta/field_info.dart';
+import 'package:fory/src/meta/field_type.dart';
import 'package:fory/src/meta/type_def.dart';
import 'package:fory/src/meta/type_ids.dart';
import 'package:fory/src/types/int64.dart';
@@ -175,27 +175,79 @@ Int64 typeDefHeader(
}
int schemaHash(TypeDef typeDef) {
+ return _murmurHash3X64_128Bits(utf8.encode(schemaFingerprint(typeDef)))
+ .$1
+ .low32;
+}
+
+String schemaFingerprint(TypeDef typeDef) {
final parts = typeDef.fields
.map(
(field) => StringBuffer()
..write(field.identifier)
..write(',')
- ..write(_fingerprintTypeId(field))
- ..write(',')
- ..write(field.fieldType.ref ? '1' : '0')
- ..write(',')
- ..write(field.fieldType.nullable ? '1' : '0')
+ ..write(
+ _fieldTypeFingerprint(
+ field.fieldType,
+ includeRef: true,
+ includeNullable: true,
+ ),
+ )
..write(';'),
)
.map((buffer) => buffer.toString())
.toList(growable: false)
..sort();
- return _murmurHash3X64_128Bits(utf8.encode(parts.join())).$1.low32;
+ return parts.join();
+}
+
+String _fieldTypeFingerprint(
+ FieldType fieldType, {
+ required bool includeRef,
+ required bool includeNullable,
+}) {
+ final buffer = StringBuffer()
+ ..write(_fingerprintTypeId(fieldType))
+ ..write(',')
+ ..write(includeRef && fieldType.ref ? '1' : '0')
+ ..write(',')
+ ..write(includeNullable && fieldType.nullable ? '1' : '0');
+ if (fieldType.arguments.isNotEmpty) {
+ buffer.write('[');
+ if (fieldType.typeId == TypeIds.map) {
+ buffer
+ ..write(
+ _fieldTypeFingerprint(
+ fieldType.arguments[0],
+ includeRef: false,
+ includeNullable: false,
+ ),
+ )
+ ..write('|')
+ ..write(
+ _fieldTypeFingerprint(
+ fieldType.arguments[1],
+ includeRef: false,
+ includeNullable: false,
+ ),
+ );
+ } else {
+ buffer.write(
+ _fieldTypeFingerprint(
+ fieldType.arguments.single,
+ includeRef: false,
+ includeNullable: false,
+ ),
+ );
+ }
+ buffer.write(']');
+ }
+ return buffer.toString();
}
-int _fingerprintTypeId(FieldInfo field) {
- final typeId = field.fieldType.typeId;
- if (field.fieldType.isDynamic || typeId == TypeIds.unknown) {
+int _fingerprintTypeId(FieldType fieldType) {
+ final typeId = fieldType.typeId;
+ if (fieldType.isDynamic || typeId == TypeIds.unknown) {
return TypeIds.unknown;
}
switch (typeId) {
diff --git a/dart/packages/fory/lib/src/util/int_validation.dart b/dart/packages/fory/lib/src/util/int_validation.dart
new file mode 100644
index 000000000..dc670b199
--- /dev/null
+++ b/dart/packages/fory/lib/src/util/int_validation.dart
@@ -0,0 +1,66 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+@pragma('vm:prefer-inline')
+int checkedInt8(int value) {
+ if (value < -128 || value > 127) {
+ throw RangeError.range(value, -128, 127, 'value');
+ }
+ return value;
+}
+
+@pragma('vm:prefer-inline')
+int checkedInt16(int value) {
+ if (value < -32768 || value > 32767) {
+ throw RangeError.range(value, -32768, 32767, 'value');
+ }
+ return value;
+}
+
+@pragma('vm:prefer-inline')
+int checkedInt32(int value) {
+ if (value < -2147483648 || value > 2147483647) {
+ throw RangeError.range(value, -2147483648, 2147483647, 'value');
+ }
+ return value;
+}
+
+@pragma('vm:prefer-inline')
+int checkedUint8(int value) {
+ if (value < 0 || value > 255) {
+ throw RangeError.range(value, 0, 255, 'value');
+ }
+ return value;
+}
+
+@pragma('vm:prefer-inline')
+int checkedUint16(int value) {
+ if (value < 0 || value > 65535) {
+ throw RangeError.range(value, 0, 65535, 'value');
+ }
+ return value;
+}
+
+@pragma('vm:prefer-inline')
+int checkedUint32(int value) {
+ if (value < 0 || value > 4294967295) {
+ throw RangeError.range(value, 0, 4294967295, 'value');
+ }
+ return value;
+}
diff --git a/dart/packages/fory/test/container_ref_test.dart b/dart/packages/fory/test/container_ref_test.dart
index a58da73f8..4fbdf366f 100644
--- a/dart/packages/fory/test/container_ref_test.dart
+++ b/dart/packages/fory/test/container_ref_test.dart
@@ -29,16 +29,16 @@ class Node {
String name = '';
}
-/// Struct with @ListType that enables ref tracking on list elements.
+/// Struct with @ListField that enables ref tracking on list elements.
@ForyStruct()
class RefListContainer {
RefListContainer();
- @ListType(element: ValueType.ref())
+ @ListField(element: DeclaredType(ref: true))
List<Node> items = <Node>[];
}
-/// Struct with no @ListType annotation — list elements are NOT ref-tracked.
+/// Struct with no @ListField annotation — list elements are NOT ref-tracked.
@ForyStruct()
class NoRefListContainer {
NoRefListContainer();
@@ -46,16 +46,16 @@ class NoRefListContainer {
List<Node> items = <Node>[];
}
-/// Struct with @MapType that enables ref tracking on map values.
+/// Struct with @MapField that enables ref tracking on map values.
@ForyStruct()
class RefMapValueContainer {
RefMapValueContainer();
- @MapType(value: ValueType.ref())
+ @MapField(value: DeclaredType(ref: true))
Map<String, Node> entries = <String, Node>{};
}
-/// Struct with no @MapType — map values are NOT ref-tracked.
+/// Struct with no @MapField — map values are NOT ref-tracked.
@ForyStruct()
class NoRefMapContainer {
NoRefMapContainer();
@@ -63,12 +63,12 @@ class NoRefMapContainer {
Map<String, Node> entries = <String, Node>{};
}
-/// Struct with @MapType that enables ref tracking on map keys.
+/// Struct with @MapField that enables ref tracking on map keys.
@ForyStruct()
class RefMapKeyContainer {
RefMapKeyContainer();
- @MapType(key: ValueType.ref())
+ @MapField(key: DeclaredType(ref: true))
Map<Node, String> entries = <Node, String>{};
}
@@ -77,8 +77,8 @@ class RefMapKeyContainer {
class NestedListOfMapContainer {
NestedListOfMapContainer();
- @ListType(
- element: MapType(value: ValueType.ref()),
+ @ListField(
+ element: MapType(value: DeclaredType(ref: true)),
)
List<Map<String, Node>> groups = <Map<String, Node>>[];
}
@@ -88,8 +88,8 @@ class NestedListOfMapContainer {
class NestedMapOfListContainer {
NestedMapOfListContainer();
- @MapType(
- value: ListType(element: ValueType.ref()),
+ @MapField(
+ value: ListType(element: DeclaredType(ref: true)),
)
Map<String, List<Node>> groups = <String, List<Node>>{};
}
@@ -121,7 +121,7 @@ void main() {
_registerAll(fory);
});
- group('list element ref via @ListType annotation', () {
+ group('list element ref via @ListField annotation', () {
test('shared list elements preserve identity with element ref enabled', () {
final shared = Node()..name = 'shared';
final container = RefListContainer()
@@ -150,7 +150,7 @@ void main() {
});
});
- group('map value ref via @MapType annotation', () {
+ group('map value ref via @MapField annotation', () {
test('shared map values preserve identity with value ref enabled', () {
final shared = Node()..name = 'val';
final container = RefMapValueContainer()
@@ -177,7 +177,7 @@ void main() {
});
});
- group('map key ref via @MapType annotation', () {
+ group('map key ref via @MapField annotation', () {
test('shared map keys preserve identity with key ref enabled', () {
final shared = Node()..name = 'key';
final container = RefMapKeyContainer()
@@ -191,7 +191,7 @@ void main() {
});
});
- group('nested container ref via @ListType/@MapType annotation', () {
+ group('nested container ref via @ListField/@MapField annotation', () {
test('list of maps with ref-tracked values preserves identity across maps',
() {
final shared = Node()..name = 'deep';
@@ -208,7 +208,8 @@ void main() {
expect(identical(result.groups[0]['x'], result.groups[1]['y']), isTrue);
});
- test('map of lists with ref-tracked elements preserves identity across lists',
+ test(
+ 'map of lists with ref-tracked elements preserves identity across lists',
() {
final shared = Node()..name = 'maplist';
final container = NestedMapOfListContainer()
diff --git a/dart/packages/fory/test/manual_registration_test.dart b/dart/packages/fory/test/manual_registration_test.dart
index 1558478e3..d18ff9d4f 100644
--- a/dart/packages/fory/test/manual_registration_test.dart
+++ b/dart/packages/fory/test/manual_registration_test.dart
@@ -194,7 +194,7 @@ void main() {
maxCollectionSize: 1024,
maxBinarySize: 4096,
);
- final bytes = fory.serialize(Int32(42));
- expect(fory.deserialize<Int32>(bytes), equals(Int32(42)));
+ final bytes = fory.serialize((42));
+ expect(fory.deserialize<int>(bytes), equals((42)));
});
}
diff --git a/dart/packages/fory/test/nested_type_spec_test.dart b/dart/packages/fory/test/nested_type_spec_test.dart
new file mode 100644
index 000000000..4aa2929d9
--- /dev/null
+++ b/dart/packages/fory/test/nested_type_spec_test.dart
@@ -0,0 +1,274 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import 'package:fory/fory.dart';
+import 'package:fory/src/serializer/compatible_struct_metadata.dart';
+import 'package:test/test.dart';
+
+part 'nested_type_spec_test.fory.dart';
+
+@ForyStruct()
+class NestedFixedContainer {
+ NestedFixedContainer();
+
+ @MapField(
+ value: ListType(
+ element: Int32Type(encoding: Encoding.fixed),
+ ),
+ )
+ Map<String, List<int?>> nested = <String, List<int?>>{};
+}
+
+@ForyStruct()
+class NestedVarintContainer {
+ NestedVarintContainer();
+
+ @MapField(
+ value: ListType(
+ element: Int32Type(encoding: Encoding.varint),
+ ),
+ )
+ Map<String, List<int?>> nested = <String, List<int?>>{};
+}
+
+@ForyStruct()
+class NestedRefNode {
+ NestedRefNode();
+
+ String name = '';
+}
+
+@ForyStruct()
+class NestedRefWriterContainer {
+ NestedRefWriterContainer();
+
+ @ListField(element: DeclaredType(ref: true))
+ List<NestedRefNode> items = <NestedRefNode>[];
+}
+
+@ForyStruct()
+class NestedRefReaderContainer {
+ NestedRefReaderContainer();
+
+ List<NestedRefNode> items = <NestedRefNode>[];
+}
+
+@ForyStruct()
+class NestedNullableWriterContainer {
+ NestedNullableWriterContainer();
+
+ @ListField(element: StringType(nullable: true))
+ List<String?> items = <String?>[];
+}
+
+@ForyStruct()
+class NestedNullableReaderContainer {
+ NestedNullableReaderContainer();
+
+ List<String> items = <String>[];
+}
+
+void _registerFixedCompatible(Fory fory) {
+ NestedTypeSpecTestFory.register(
+ fory,
+ NestedFixedContainer,
+ namespace: 'nested',
+ typeName: 'NestedIntContainer',
+ );
+}
+
+void _registerVarintConsistent(Fory fory) {
+ NestedTypeSpecTestFory.register(
+ fory,
+ NestedVarintContainer,
+ namespace: 'nested',
+ typeName: 'NestedIntContainer',
+ );
+}
+
+void _registerRefWriter(Fory fory) {
+ NestedTypeSpecTestFory.register(
+ fory,
+ NestedRefNode,
+ namespace: 'nested',
+ typeName: 'NestedRefNode',
+ );
+ NestedTypeSpecTestFory.register(
+ fory,
+ NestedRefWriterContainer,
+ namespace: 'nested',
+ typeName: 'NestedRefContainer',
+ );
+}
+
+void _registerRefReader(Fory fory) {
+ NestedTypeSpecTestFory.register(
+ fory,
+ NestedRefNode,
+ namespace: 'nested',
+ typeName: 'NestedRefNode',
+ );
+ NestedTypeSpecTestFory.register(
+ fory,
+ NestedRefReaderContainer,
+ namespace: 'nested',
+ typeName: 'NestedRefContainer',
+ );
+}
+
+void _registerNullableWriter(Fory fory) {
+ NestedTypeSpecTestFory.register(
+ fory,
+ NestedNullableWriterContainer,
+ namespace: 'nested',
+ typeName: 'NestedNullableContainer',
+ );
+}
+
+void _registerNullableReader(Fory fory) {
+ NestedTypeSpecTestFory.register(
+ fory,
+ NestedNullableReaderContainer,
+ namespace: 'nested',
+ typeName: 'NestedNullableContainer',
+ );
+}
+
+void main() {
+ group('nested type specs', () {
+ test(
+ 'compatible mode reads nested overridden fixed int32 list values from remote meta',
+ () {
+ final writer = Fory(compatible: true);
+ final reader = Fory(compatible: true);
+ _registerFixedCompatible(writer);
+ _registerVarintConsistent(reader);
+
+ final result = reader.deserialize<NestedVarintContainer>(
+ writer.serialize(
+ NestedFixedContainer()
+ ..nested = <String, List<int?>>{
+ 'a': <int?>[1, null, -7],
+ },
+ ),
+ );
+
+ expect(result.nested['a'], orderedEquals(<int?>[1, null, -7]));
+
+ final remoteTypeDef = CompatibleStructMetadata.remoteTypeDefFor(result);
+ expect(remoteTypeDef, isNotNull);
+ final nestedField = remoteTypeDef!.fields.single;
+ expect(nestedField.fieldType.typeId, equals(TypeIds.map));
+ expect(nestedField.fieldType.arguments[0].typeId, equals(TypeIds.string));
+ final remoteList = nestedField.fieldType.arguments[1];
+ expect(remoteList.typeId, equals(TypeIds.list));
+ final remoteElement = remoteList.arguments.single;
+ expect(remoteElement.typeId, equals(TypeIds.int32));
+ expect(remoteElement.nullable, isTrue);
+ });
+
+ test('schema-consistent mode rejects nested encoding hash mismatches', () {
+ final writer = Fory();
+ final reader = Fory();
+ _registerFixedCompatible(writer);
+ _registerVarintConsistent(reader);
+
+ final bytes = writer.serialize(
+ NestedFixedContainer()
+ ..nested = <String, List<int?>>{
+ 'a': <int?>[1, null, -7],
+ },
+ );
+
+ expect(
+ () => reader.deserialize<NestedVarintContainer>(bytes),
+ throwsA(
+ isA<StateError>().having(
+ (error) => error.toString(),
+ 'message',
+ contains('Struct schema version mismatch'),
+ ),
+ ),
+ );
+ });
+
+ test(
+ 'nested ref metadata stays out of schema hash but survives remote meta',
+ () {
+ final writer = Fory(compatible: true);
+ final reader = Fory(compatible: true);
+ _registerRefWriter(writer);
+ _registerRefReader(reader);
+
+ final shared = NestedRefNode()..name = 'shared';
+ final result = reader.deserialize<NestedRefReaderContainer>(
+ writer.serialize(
+ NestedRefWriterContainer()..items = <NestedRefNode>[shared, shared],
+ trackRef: true,
+ ),
+ );
+
+ expect(result.items, hasLength(2));
+ expect(result.items[0].name, equals('shared'));
+ expect(identical(result.items[0], result.items[1]), isTrue);
+
+ final remoteTypeDef = CompatibleStructMetadata.remoteTypeDefFor(result);
+ expect(remoteTypeDef, isNotNull);
+ final remoteElement =
+ remoteTypeDef!.fields.single.fieldType.arguments.single;
+ expect(remoteElement.ref, isTrue);
+ });
+
+ test('schema-consistent mode ignores nested ref-only schema differences',
+ () {
+ final writer = Fory();
+ final reader = Fory();
+ _registerRefWriter(writer);
+ _registerRefReader(reader);
+
+ final shared = NestedRefNode()..name = 'shared';
+ final result = reader.deserialize<NestedRefReaderContainer>(
+ writer.serialize(
+ NestedRefWriterContainer()..items = <NestedRefNode>[shared, shared],
+ trackRef: true,
+ ),
+ );
+
+ expect(result.items, hasLength(2));
+ expect(identical(result.items[0], result.items[1]), isTrue);
+ });
+
+ test(
+ 'schema-consistent mode ignores nested nullable-only schema differences',
+ () {
+ final writer = Fory();
+ final reader = Fory();
+ _registerNullableWriter(writer);
+ _registerNullableReader(reader);
+
+ final result = reader.deserialize<NestedNullableReaderContainer>(
+ writer.serialize(
+ NestedNullableWriterContainer()..items = <String?>['a', 'b'],
+ ),
+ );
+
+ expect(result.items, orderedEquals(<String>['a', 'b']));
+ });
+ });
+}
diff --git a/dart/packages/fory/test/numeric_wrapper_test.dart b/dart/packages/fory/test/numeric_wrapper_test.dart
deleted file mode 100644
index f5f0436c8..000000000
--- a/dart/packages/fory/test/numeric_wrapper_test.dart
+++ /dev/null
@@ -1,475 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-import 'dart:typed_data';
-
-import 'package:fory/fory.dart';
-import 'package:fory/src/resolver/type_resolver.dart';
-import 'package:fory/src/serializer/compatible_struct_metadata.dart';
-import 'package:test/test.dart';
-
-part 'numeric_wrapper_test.fory.dart';
-
-Uint64 _u64Hex(String value) => Uint64.parseHex(value);
-
-double _float64FromWords(int low32, int high32) {
- final bytes = ByteData(8)
- ..setUint32(0, low32, Endian.little)
- ..setUint32(4, high32, Endian.little);
- return bytes.getFloat64(0, Endian.little);
-}
-
-@ForyStruct()
-class NumericWrappersEnvelope {
- NumericWrappersEnvelope();
-
- Int8 i8 = Int8(0);
- Int16 i16 = Int16(0);
- Int32 i32 = Int32(0);
- Int64 i64 = Int64(0);
- Uint8 u8 = Uint8(0);
- Uint16 u16 = Uint16(0);
- Uint32 u32 = Uint32(0);
- Uint64 u64 = Uint64(0);
- Float16 half = Float16(0);
- Bfloat16 brain = Bfloat16(0);
- Float32 single = Float32(0);
- Int8? optionalI8;
- Uint64? optionalU64;
- Float16? optionalHalf;
- Bfloat16? optionalBrain;
- Float32? optionalSingle;
-}
-
-@ForyStruct()
-class NumericWrappersMetadataReader {
- NumericWrappersMetadataReader();
-
- Int8 i8 = Int8(0);
-}
-
-void _registerNumericWrappers(Fory fory) {
- NumericWrapperTestFory.register(
- fory,
- NumericWrappersEnvelope,
- namespace: 'test',
- typeName: 'NumericWrappersEnvelope',
- );
-}
-
-void _registerNumericWrappersMetadataReader(Fory fory) {
- NumericWrapperTestFory.register(
- fory,
- NumericWrappersMetadataReader,
- namespace: 'test',
- typeName: 'NumericWrappersEnvelope',
- );
-}
-
-T _roundTrip<T>(Fory fory, T value) =>
- fory.deserialize<T>(fory.serialize(value));
-
-NumericWrappersEnvelope _sampleEnvelope() {
- return NumericWrappersEnvelope()
- ..i8 = Int8(-127)
- ..i16 = Int16(0x7fff)
- ..i32 = Int32(-2147483648)
- ..i64 = Int64.parseHex('8000000000000000')
- ..u8 = Uint8(0xff)
- ..u16 = Uint16(0xffff)
- ..u32 = Uint32(0xffffffff)
- ..u64 = _u64Hex('ffffffffffffffff')
- ..half = Float16.fromBits(0x3555)
- ..brain = Bfloat16.fromBits(0x3eab)
- ..single = Float32.fromBits(0x40490fdb)
- ..optionalI8 = Int8(126)
- ..optionalU64 = _u64Hex('8000000000000000')
- ..optionalHalf = Float16.fromBits(0x8000)
- ..optionalBrain = Bfloat16.fromBits(0x8000)
- ..optionalSingle = Float32.fromBits(0x80000000);
-}
-
-void _expectEnvelopeEquals(
- NumericWrappersEnvelope actual,
- NumericWrappersEnvelope expected,
-) {
- expect(actual.i8, equals(expected.i8));
- expect(actual.i16, equals(expected.i16));
- expect(actual.i32, equals(expected.i32));
- expect(actual.i64, equals(expected.i64));
- expect(actual.u8, equals(expected.u8));
- expect(actual.u16, equals(expected.u16));
- expect(actual.u32, equals(expected.u32));
- expect(actual.u64, equals(expected.u64));
- expect(actual.half.toBits(), equals(expected.half.toBits()));
- expect(actual.brain.toBits(), equals(expected.brain.toBits()));
- expect(actual.single.toBits(), equals(expected.single.toBits()));
- expect(actual.optionalI8, equals(expected.optionalI8));
- expect(actual.optionalU64, equals(expected.optionalU64));
- expect(
- actual.optionalHalf?.toBits(), equals(expected.optionalHalf?.toBits()));
- expect(
- actual.optionalBrain?.toBits(), equals(expected.optionalBrain?.toBits()));
- expect(
- actual.optionalSingle?.toBits(),
- equals(expected.optionalSingle?.toBits()),
- );
-}
-
-int _remoteFieldTypeId(Object value, String identifier) {
- final remoteTypeDef = CompatibleStructMetadata.remoteTypeDefFor(value);
- expect(remoteTypeDef, isNotNull);
- final field = remoteTypeDef!.fields.firstWhere(
- (field) => field.identifier == identifier,
- );
- return field.fieldType.typeId;
-}
-
-void main() {
- group('numeric wrappers', () {
- test('signed integer wrappers normalize arithmetic and bitwise operations',
- () {
- expect(Int8(127) + 1, equals(Int8(-128)));
- expect(Int8(-128) - 1, equals(Int8(127)));
- expect(Int8(12) * Int8(11), equals(Int8(-124)));
- expect(Int8(7) ~/ 2, equals(Int8(3)));
- expect(Int8(7) / 2, equals(3.5));
- expect(Int8(0x0f) & Int8(0x33), equals(Int8(0x03)));
- expect(Int8(0x0f) | Int8(0x33), equals(Int8(0x3f)));
- expect(Int8(0x0f) ^ Int8(0x33), equals(Int8(0x3c)));
- expect(Int8(0x40) >> 1, equals(Int8(0x20)));
- expect(Int8(0x40) << 2, equals(Int8(0x00)));
- expect(Int8(-1) < 0, isTrue);
- expect(Int8(10) >= Int8(10), isTrue);
-
- expect(Int16(0x7fff) + 1, equals(Int16(-0x8000)));
- expect(Int16(-0x8000) - 1, equals(Int16(0x7fff)));
- expect(Int32(0x7fffffff) + 1, equals(Int32(-0x80000000)));
- expect(Int32(-0x80000000) - 1, equals(Int32(0x7fffffff)));
- expect(Int32(0x12345678) >> 4, equals(Int32(0x01234567)));
- expect(Int32(123).toInt(), equals(123));
- });
-
- test(
- 'unsigned integer wrappers normalize arithmetic and bitwise operations',
- () {
- expect(Uint8(0xff) + 1, equals(Uint8(0)));
- expect(Uint8(0) - 1, equals(Uint8(0xff)));
- expect(Uint8(0xf0) >> 4, equals(Uint8(0x0f)));
- expect(Uint8(0xf0) >>> 4, equals(Uint8(0x0f)));
- expect(Uint8(0xff) >> 1, equals(Uint8(0x7f)));
- expect(Uint8(0xff) >>> 1, equals(Uint8(0x7f)));
- expect(-Uint8(1), equals(Uint8(0xff)));
-
- expect(Uint16(0xffff) + 1, equals(Uint16(0)));
- expect(Uint16(0) - 1, equals(Uint16(0xffff)));
- expect(Uint16(0xffff) >> 1, equals(Uint16(0x7fff)));
- expect(Uint16(0xffff) >>> 1, equals(Uint16(0x7fff)));
-
- expect(Uint32(0xffffffff) + 1, equals(Uint32(0)));
- expect(Uint32(0) - 1, equals(Uint32(0xffffffff)));
- expect(
- Uint32(0xf0f0f0f0) & Uint32(0x0ff00ff0), equals(Uint32(0x00f000f0)));
- expect(Uint32(0xffffffff) >> 1, equals(Uint32(0x7fffffff)));
- expect(Uint32(0xffffffff) >>> 1, equals(Uint32(0x7fffffff)));
-
- expect(_u64Hex('ffffffffffffffff') + 1, equals(Uint64(0)));
- expect(Uint64(0) - 1, equals(_u64Hex('ffffffffffffffff')));
- expect(
- _u64Hex('123456789abcdef0') >> 4,
- equals(_u64Hex('0123456789abcdef')),
- );
- expect(Uint64(0xff).toInt(), equals(0xff));
- });
-
- test('Float16 arithmetic rounds back to binary16 precision', () {
- expect((Float16(1.5) + 2).toBits(), equals(Float16(3.5).toBits()));
- expect(
- (Float16(7) - Float16(2.5)).toBits(), equals(Float16(4.5).toBits()));
- expect((Float16(1) / 3).toBits(), equals(Float16(1 / 3).toBits()));
- expect((Float16(7.5) % 2).toBits(), equals(Float16(1.5).toBits()));
- expect(Float16(7.5) ~/ 2, equals(3));
- expect((-Float16(1.5)).toBits(), equals(Float16(-1.5).toBits()));
- expect(Float16(-1) < 0, isTrue);
- expect(Float16(3.5) >= Float16(3.5), isTrue);
- expect(Float16.fromBits(0x3555).value, closeTo(0.333251953125, 1e-12));
- });
-
- test('Bfloat16 arithmetic rounds back to bfloat16 precision', () {
- expect((Bfloat16(1.5) + 2).toBits(), equals(Bfloat16(3.5).toBits()));
- expect(
- (Bfloat16(7) - Bfloat16(2.5)).toBits(),
- equals(Bfloat16(4.5).toBits()),
- );
- expect((Bfloat16(1) / 3).toBits(), equals(Bfloat16(1 / 3).toBits()));
- expect((Bfloat16(7.5) % 2).toBits(), equals(Bfloat16(1.5).toBits()));
- expect(Bfloat16(7.5) ~/ 2, equals(3));
- expect((-Bfloat16(1.5)).toBits(), equals(Bfloat16(-1.5).toBits()));
- expect(Bfloat16(-1) < 0, isTrue);
- expect(Bfloat16(3.5) >= Bfloat16(3.5), isTrue);
- expect(Bfloat16.fromBits(0x3eab).value, closeTo(0.333984375, 1e-12));
- });
-
- test(
- 'Bfloat16.fromDouble rounds directly from float64 and preserves NaN sign payload bits',
- () {
- final trickySubnormal = _float64FromWords(0x7e281cc1, 0x37da834f);
- final payloadNaN = _float64FromWords(0x6789abcd, 0xfff12345);
- final convertedNaN = Bfloat16.fromDouble(payloadNaN);
-
- expect(Bfloat16.fromDouble(trickySubnormal).toBits(), equals(0x0007));
- expect(convertedNaN.toBits() & 0x8000, equals(0x8000));
- expect(convertedNaN.toBits() & 0x7f80, equals(0x7f80));
- expect(convertedNaN.toBits() & 0x0040, equals(0x0040));
- expect(convertedNaN.toBits() & 0x003f, isNot(0));
- });
-
- test('Float32 arithmetic rounds back to binary32 precision', () {
- expect((Float32(1.5) + 2).toBits(), equals(Float32(3.5).toBits()));
- expect(
- (Float32(7) - Float32(2.5)).toBits(), equals(Float32(4.5).toBits()));
- expect((Float32(1) / 3).toBits(), equals(Float32(1 / 3).toBits()));
- expect((Float32(7.5) % 2).toBits(), equals(Float32(1.5).toBits()));
- expect(Float32(7.5) ~/ 2, equals(3));
- expect((-Float32(1.5)).toBits(), equals(Float32(-1.5).toBits()));
- expect(Float32(-1) < 0, isTrue);
- expect(Float32(3.5) >= Float32(3.5), isTrue);
- expect(Float32.fromBits(0x3eaaaaab).value, closeTo(1 / 3, 1e-8));
- });
-
- test('round-trips root fixed-width wrappers', () {
- final fory = Fory();
-
- expect(_roundTrip<Int8>(fory, Int8(-129)), equals(Int8(127)));
- expect(_roundTrip<Int16>(fory, Int16(-32769)), equals(Int16(32767)));
- expect(
- _roundTrip<Int32>(fory, Int32(0x80000000)),
- equals(Int32(-0x80000000)),
- );
- expect(_roundTrip<Uint8>(fory, Uint8(-1)), equals(Uint8(0xff)));
- expect(_roundTrip<Uint16>(fory, Uint16(-1)), equals(Uint16(0xffff)));
- expect(_roundTrip<Uint32>(fory, Uint32(-1)), equals(Uint32(0xffffffff)));
- expect(_roundTrip<Int64>(fory, Int64(-1)), equals(Int64(-1)));
- expect(
- _roundTrip<Int64>(fory, Int64.parseHex('8000000000000000')),
- equals(Int64.parseHex('8000000000000000')),
- );
- expect(
- _roundTrip<Int64>(fory, Int64.parseHex('7fffffffffffffff')),
- equals(Int64.parseHex('7fffffffffffffff')),
- );
- expect(
- _roundTrip<Uint64>(fory, Uint64(-1)),
- equals(_u64Hex('ffffffffffffffff')),
- );
- });
-
- test('resolves root numeric wrappers to compact varint wire ids', () {
- final resolver = TypeResolver(Config());
-
- expect(resolver.resolveValue(1).typeId, equals(TypeIds.varInt64));
- expect(resolver.resolveValue(Int32(1)).typeId, equals(TypeIds.varInt32));
- expect(resolver.resolveValue(Int64(1)).typeId, equals(TypeIds.varInt64));
- expect(
- resolver.resolveValue(Uint32(1)).typeId,
- equals(TypeIds.varUint32),
- );
- if (identical(1, 1.0)) {
- expect(
- resolver.resolveValue(Uint64(1)).typeId,
- equals(TypeIds.varUint64),
- );
- } else {
- expect(
- resolver.resolveValue(Uint64(1)).typeId,
- equals(TypeIds.varInt64),
- reason: 'Native Uint64 is an extension type represented as int.',
- );
- }
- });
-
- test('typed root Int64 reads preserve wrappers', () {
- final fory = Fory();
-
- expect(
- fory.deserialize<int>(fory.serialize(1)),
- equals(1),
- reason: 'Plain int roots still decode as Dart int.',
- );
- expect(
- fory.deserialize<Int64>(fory.serialize(Int64(-1))),
- equals(Int64(-1)),
- );
- expect(
- fory.deserialize<Int64>(
- fory.serialize(Int64.parseHex('8000000000000000')),
- ),
- equals(Int64.parseHex('8000000000000000')),
- );
- expect(
- fory.deserialize<Int64>(
- fory.serialize(Int64.parseHex('7fffffffffffffff')),
- ),
- equals(Int64.parseHex('7fffffffffffffff')),
- );
- expect(
- fory.deserialize<Int64?>(
- fory.serialize(Int64.parseHex('8000000000000000')),
- ),
- equals(Int64.parseHex('8000000000000000')),
- );
- expect(
- fory.deserialize<int?>(fory.serialize(Int64(-1))),
- equals(-1),
- reason: 'Nullable int roots still decode as Dart int.',
- );
- });
-
- test('web dynamic Uint64 wrappers keep unsigned metadata', () {
- if (!identical(1, 1.0)) {
- return;
- }
-
- final fory = Fory();
- final value = _u64Hex('ffffffffffffffff');
-
- expect(
- fory.deserialize<Uint64>(fory.serialize(value)),
- equals(value),
- );
-
- final list = fory.deserialize<Object?>(
- fory.serialize(<Object?>[value]),
- ) as List;
- expect(list.single, equals(value));
-
- final map = fory.deserialize<Object?>(
- fory.serialize(<Object?, Object?>{'value': value}),
- ) as Map;
- expect(map['value'], equals(value));
- });
-
- test('round-trips root Float16 payloads with exact bits', () {
- final fory = Fory();
- final cases = <Float16>[
- Float16.fromBits(0x0000),
- Float16.fromBits(0x8000),
- Float16.fromBits(0x3555),
- Float16.fromBits(0x7c00),
- Float16.fromBits(0x7e00),
- ];
-
- for (final value in cases) {
- expect(
- _roundTrip<Float16>(fory, value).toBits(), equals(value.toBits()));
- }
- });
-
- test('round-trips root Bfloat16 payloads with exact bits', () {
- final fory = Fory();
- final cases = <Bfloat16>[
- Bfloat16.fromBits(0x0000),
- Bfloat16.fromBits(0x8000),
- Bfloat16.fromBits(0x3eab),
- Bfloat16.fromBits(0x7f80),
- Bfloat16.fromBits(0x7fc0),
- ];
-
- for (final value in cases) {
- expect(
- _roundTrip<Bfloat16>(fory, value).toBits(),
- equals(value.toBits()),
- );
- }
- });
-
- test('round-trips root Float32 payloads with exact bits', () {
- final fory = Fory();
- final cases = <Float32>[
- Float32.fromBits(0x00000000),
- Float32.fromBits(0x80000000),
- Float32.fromBits(0x3eaaaaab),
- Float32.fromBits(0x7f800000),
- Float32.fromBits(0x7fc00000),
- ];
-
- for (final value in cases) {
- expect(
- _roundTrip<Float32>(fory, value).toBits(), equals(value.toBits()));
- }
- });
-
- test(
- 'round-trips generated numeric wrapper fields in schema-consistent mode',
- () {
- final fory = Fory();
- _registerNumericWrappers(fory);
-
- final value = _sampleEnvelope();
- final roundTrip =
- fory.deserialize<NumericWrappersEnvelope>(fory.serialize(value));
-
- _expectEnvelopeEquals(roundTrip, value);
- });
-
- test('round-trips generated numeric wrapper fields in compatible mode', () {
- final fory = Fory(compatible: true);
- _registerNumericWrappers(fory);
-
- final value = _sampleEnvelope();
- final roundTrip =
- fory.deserialize<NumericWrappersEnvelope>(fory.serialize(value));
-
- _expectEnvelopeEquals(roundTrip, value);
- });
-
- test('compatible metadata records numeric wrapper varint defaults', () {
- final writer = Fory(compatible: true);
- final reader = Fory(compatible: true);
- _registerNumericWrappers(writer);
- _registerNumericWrappersMetadataReader(reader);
-
- final roundTrip = reader.deserialize<NumericWrappersMetadataReader>(
- writer.serialize(_sampleEnvelope()),
- );
-
- expect(roundTrip.i8, equals(Int8(-127)));
- expect(_remoteFieldTypeId(roundTrip, 'i32'), equals(TypeIds.varInt32));
- expect(_remoteFieldTypeId(roundTrip, 'i64'), equals(TypeIds.varInt64));
- expect(_remoteFieldTypeId(roundTrip, 'u32'), equals(TypeIds.varUint32));
- expect(_remoteFieldTypeId(roundTrip, 'u64'), equals(TypeIds.varUint64));
- });
-
- test('supports null optional numeric wrapper fields', () {
- final fory = Fory();
- _registerNumericWrappers(fory);
-
- final roundTrip = _roundTrip<NumericWrappersEnvelope>(
- fory,
- NumericWrappersEnvelope(),
- );
-
- expect(roundTrip.optionalI8, isNull);
- expect(roundTrip.optionalU64, isNull);
- expect(roundTrip.optionalHalf, isNull);
- expect(roundTrip.optionalBrain, isNull);
- expect(roundTrip.optionalSingle, isNull);
- });
- });
-}
diff --git a/dart/packages/fory/test/runtime_validation_test.dart b/dart/packages/fory/test/runtime_validation_test.dart
index 4e1c39509..347bbc63b 100644
--- a/dart/packages/fory/test/runtime_validation_test.dart
+++ b/dart/packages/fory/test/runtime_validation_test.dart
@@ -282,12 +282,12 @@ void main() {
final bytes = fory.serialize('value');
expect(
- () => fory.deserialize<Int32>(bytes),
+ () => fory.deserialize<int>(bytes),
throwsA(
isA<StateError>().having(
(error) => error.toString(),
'message',
- contains('expected Int32'),
+ contains('expected int'),
),
),
);
diff --git a/dart/packages/fory/test/signed_serializer_test.dart b/dart/packages/fory/test/signed_serializer_test.dart
index 176854f36..4be24b1e7 100644
--- a/dart/packages/fory/test/signed_serializer_test.dart
+++ b/dart/packages/fory/test/signed_serializer_test.dart
@@ -35,45 +35,45 @@ class SignedFields {
int plainInt = 0;
- @Int32Type(compress: true)
+ @ForyField(type: Int32Type(encoding: Encoding.varint))
int i32Var = 0;
- @Int32Type(compress: false)
+ @ForyField(type: Int32Type(encoding: Encoding.fixed))
int i32Fixed = 0;
- @Int64Type(encoding: LongEncoding.varint)
+ @ForyField(type: Int64Type(encoding: Encoding.varint))
int i64VarInt = 0;
- @Int64Type(encoding: LongEncoding.fixed)
+ @ForyField(type: Int64Type(encoding: Encoding.fixed))
int i64FixedInt = 0;
- @Int64Type(encoding: LongEncoding.tagged)
+ @ForyField(type: Int64Type(encoding: Encoding.tagged))
int i64TaggedInt = 0;
Int64 i64Default = Int64(0);
- @Int64Type(encoding: LongEncoding.varint)
+ @ForyField(type: Int64Type(encoding: Encoding.varint))
Int64 i64Var = Int64(0);
- @Int64Type(encoding: LongEncoding.fixed)
+ @ForyField(type: Int64Type(encoding: Encoding.fixed))
Int64 i64Fixed = Int64(0);
- @Int64Type(encoding: LongEncoding.tagged)
+ @ForyField(type: Int64Type(encoding: Encoding.tagged))
Int64 i64Tagged = Int64(0);
- @Int64Type(encoding: LongEncoding.varint)
+ @ForyField(type: Int64Type(encoding: Encoding.varint))
int? optionalI64VarInt;
- @Int32Type(compress: true)
+ @ForyField(type: Int32Type(encoding: Encoding.varint))
int? optionalI32Var;
- @Int32Type(compress: false)
+ @ForyField(type: Int32Type(encoding: Encoding.fixed))
int? optionalI32Fixed;
- @Int64Type(encoding: LongEncoding.fixed)
+ @ForyField(type: Int64Type(encoding: Encoding.fixed))
Int64? optionalI64Fixed;
- @Int64Type(encoding: LongEncoding.tagged)
+ @ForyField(type: Int64Type(encoding: Encoding.tagged))
Int64? optionalI64Tagged;
}
@@ -88,13 +88,13 @@ class SignedMetadataReader {
class SignedIntFieldsReader {
SignedIntFieldsReader();
- @Int64Type(encoding: LongEncoding.varint)
+ @ForyField(type: Int64Type(encoding: Encoding.varint))
int i64Var = 0;
- @Int64Type(encoding: LongEncoding.fixed)
+ @ForyField(type: Int64Type(encoding: Encoding.fixed))
int i64Fixed = 0;
- @Int64Type(encoding: LongEncoding.tagged)
+ @ForyField(type: Int64Type(encoding: Encoding.tagged))
int i64Tagged = 0;
}
@@ -272,7 +272,7 @@ void main() {
}
});
- test('compatible mode round trips Int32 and Int64 encoding edge cases', () {
+ test('compatible mode round trips int and Int64 encoding edge cases', () {
final fory = Fory(compatible: true);
_registerSignedFields(fory);
@@ -372,6 +372,39 @@ void main() {
expect(_remoteFieldNullable(roundTrip, 'optional_i64_tagged'), isTrue);
});
+ test('rejects out-of-range annotated int32 fields', () {
+ final cases = <({String name, SignedFields value})>[
+ (
+ name: 'i32Var',
+ value: _smallSignedFields()..i32Var = -0x80000001,
+ ),
+ (
+ name: 'i32Fixed',
+ value: _smallSignedFields()..i32Fixed = 0x80000000,
+ ),
+ (
+ name: 'optionalI32Var',
+ value: _smallSignedFields()..optionalI32Var = 0x80000000,
+ ),
+ (
+ name: 'optionalI32Fixed',
+ value: _smallSignedFields()..optionalI32Fixed = -0x80000001,
+ ),
+ ];
+
+ for (final testCase in cases) {
+ for (final compatible in <bool>[false, true]) {
+ final fory = Fory(compatible: compatible);
+ _registerSignedFields(fory);
+ expect(
+ () => fory.serialize(testCase.value),
+ throwsA(isA<RangeError>()),
+ reason: '${testCase.name}, compatible=$compatible',
+ );
+ }
+ }
+ });
+
test('web rejects JS-unsafe Dart int fields instead of corrupting bytes',
() {
final cases = <({String name, SignedFields value})>[
diff --git a/dart/packages/fory/test/type_spec_annotation_test.dart b/dart/packages/fory/test/type_spec_annotation_test.dart
index 717810b42..8362b5a92 100644
--- a/dart/packages/fory/test/type_spec_annotation_test.dart
+++ b/dart/packages/fory/test/type_spec_annotation_test.dart
@@ -21,158 +21,103 @@ import 'package:fory/fory.dart';
import 'package:test/test.dart';
void main() {
- group('TypeOption', () {
- test('RefOption defaults tracked to true', () {
- const ref = RefOption();
- expect(ref.tracked, isTrue);
- });
-
- test('RefOption accepts explicit false', () {
- const ref = RefOption(false);
- expect(ref.tracked, isFalse);
- });
-
- test('NullableOption defaults value to true', () {
- const nullable = NullableOption();
- expect(nullable.value, isTrue);
- });
-
- test('NullableOption accepts explicit false', () {
- const nullable = NullableOption(false);
- expect(nullable.value, isFalse);
- });
-
- test('TypeOption.ref factory creates RefOption', () {
- const option = TypeOption.ref();
- expect(option, isA<RefOption>());
- expect((option as RefOption).tracked, isTrue);
- });
-
- test('TypeOption.nullable factory creates NullableOption', () {
- const option = TypeOption.nullable();
- expect(option, isA<NullableOption>());
- expect((option as NullableOption).value, isTrue);
- });
- });
-
- group('ValueType', () {
- test('default constructor has empty options', () {
- const vt = ValueType();
- expect(vt.options, isEmpty);
- });
-
- test('ValueType.ref() sets ref option', () {
- const vt = ValueType.ref();
- expect(vt.options, hasLength(1));
- expect(vt.options.first, isA<RefOption>());
- expect((vt.options.first as RefOption).tracked, isTrue);
- });
-
- test('ValueType.noRef() sets ref(false)', () {
- const vt = ValueType.noRef();
- expect(vt.options, hasLength(1));
- expect((vt.options.first as RefOption).tracked, isFalse);
- });
-
- test('ValueType.nullable() sets nullable option', () {
- const vt = ValueType.nullable();
- expect(vt.options, hasLength(1));
- expect((vt.options.first as NullableOption).value, isTrue);
- });
-
- test('ValueType.nonNullable() sets nullable(false)', () {
- const vt = ValueType.nonNullable();
- expect(vt.options, hasLength(1));
- expect((vt.options.first as NullableOption).value, isFalse);
+ group('scalar type specs', () {
+ test('DeclaredType stores leaf overrides without changing leaf kind', () {
+ const declared = DeclaredType(nullable: true, ref: true, dynamic: false);
+ expect(declared.nullable, isTrue);
+ expect(declared.ref, isTrue);
+ expect(declared.dynamic, isFalse);
+ });
+
+ test('Int32Type keeps encoding and node-local overrides separate', () {
+ const spec = Int32Type(
+ encoding: Encoding.fixed,
+ nullable: true,
+ ref: false,
+ dynamic: false,
+ );
+ expect(spec.encoding, equals(Encoding.fixed));
+ expect(spec.nullable, isTrue);
+ expect(spec.ref, isFalse);
+ expect(spec.dynamic, isFalse);
});
- test('ValueType.refNullable() sets both ref and nullable', () {
- const vt = ValueType.refNullable();
- expect(vt.options, hasLength(2));
- expect(vt.options[0], isA<RefOption>());
- expect(vt.options[1], isA<NullableOption>());
+ test('Uint64Type supports tagged encoding', () {
+ const spec = Uint64Type(encoding: Encoding.tagged);
+ expect(spec.encoding, equals(Encoding.tagged));
+ expect(spec.nullable, isNull);
+ expect(spec.ref, isNull);
+ expect(spec.dynamic, isNull);
});
});
- group('ListType', () {
- test('default constructor has empty options and default element', () {
- const lt = ListType();
- expect(lt.options, isEmpty);
- expect(lt.element, isA<ValueType>());
- expect(lt.element.options, isEmpty);
- });
-
- test('ListType.ref() sets ref option', () {
- const lt = ListType.ref();
- expect(lt.options, hasLength(1));
- expect((lt.options.first as RefOption).tracked, isTrue);
- });
-
- test('ListType.noRef() sets ref(false)', () {
- const lt = ListType.noRef();
- expect(lt.options, hasLength(1));
- expect((lt.options.first as RefOption).tracked, isFalse);
- });
-
- test('ListType.nullable() sets nullable option', () {
- const lt = ListType.nullable();
- expect(lt.options, hasLength(1));
- expect((lt.options.first as NullableOption).value, isTrue);
- });
-
- test('accepts custom element TypeSpec', () {
- const lt = ListType(element: ValueType.ref());
- expect(lt.element.options, hasLength(1));
- expect((lt.element.options.first as RefOption).tracked, isTrue);
- });
-
- test('nested MapType element', () {
+ group('container type specs', () {
+ test('ListType stores nested element specs', () {
const lt = ListType(
- options: [TypeOption.ref()],
element: MapType(
- value: ValueType.ref(),
+ value: DeclaredType(ref: true),
),
+ nullable: true,
);
- expect(lt.options, hasLength(1));
+ expect(lt.nullable, isTrue);
final mapElement = lt.element as MapType;
- expect(mapElement.value.options, hasLength(1));
- expect((mapElement.value.options.first as RefOption).tracked, isTrue);
- });
- });
-
- group('MapType', () {
- test('default constructor has empty options and default key/value', () {
- const mt = MapType();
- expect(mt.options, isEmpty);
- expect(mt.key, isA<ValueType>());
- expect(mt.value, isA<ValueType>());
+ final declaredValue = mapElement.value as DeclaredType;
+ expect(declaredValue.ref, isTrue);
});
- test('MapType.ref() sets ref option', () {
- const mt = MapType.ref();
- expect(mt.options, hasLength(1));
- expect((mt.options.first as RefOption).tracked, isTrue);
- });
-
- test('MapType.noRef() sets ref(false)', () {
- const mt = MapType.noRef();
- expect(mt.options, hasLength(1));
- expect((mt.options.first as RefOption).tracked, isFalse);
+ test('MapType stores explicit key and value trees', () {
+ const mt = MapType(
+ key: StringType(),
+ value: ListType(
+ element: Int32Type(
+ nullable: true,
+ encoding: Encoding.fixed,
+ ),
+ ),
+ ref: true,
+ );
+ expect(mt.ref, isTrue);
+ expect(mt.key, isA<StringType>());
+ final value = mt.value as ListType;
+ final element = value.element as Int32Type;
+ expect(element.nullable, isTrue);
+ expect(element.encoding, equals(Encoding.fixed));
});
+ });
- test('MapType.nullable() sets nullable option', () {
- const mt = MapType.nullable();
- expect(mt.options, hasLength(1));
- expect((mt.options.first as NullableOption).value, isTrue);
+ group('field annotations', () {
+ test('ForyField stores canonical root type overrides', () {
+ const field = ForyField(
+ id: 3,
+ nullable: true,
+ ref: false,
+ dynamic: true,
+ type: MapType(
+ key: StringType(),
+ value: ListType(
+ element: Int32Type(encoding: Encoding.fixed),
+ ),
+ ),
+ );
+ expect(field.id, equals(3));
+ expect(field.nullable, isTrue);
+ expect(field.dynamic, isTrue);
+ expect(field.type, isA<MapType>());
});
- test('accepts custom key and value TypeSpecs', () {
- const mt = MapType(
- key: ValueType.ref(),
- value: ValueType.refNullable(),
+ test('container sugar stores nested element and value overrides', () {
+ const listField = ListField(
+ element: DeclaredType(ref: true),
+ );
+ const mapField = MapField(
+ value: ListType(
+ element: Int32Type(encoding: Encoding.fixed),
+ ),
);
- expect(mt.key.options, hasLength(1));
- expect(mt.value.options, hasLength(2));
+ expect((listField.element as DeclaredType).ref, isTrue);
+ final nestedList = mapField.value as ListType;
+ final nestedElement = nestedList.element as Int32Type;
+ expect(nestedElement.encoding, equals(Encoding.fixed));
});
});
}
diff --git a/dart/packages/fory/test/unsigned_serializer_test.dart b/dart/packages/fory/test/unsigned_serializer_test.dart
index 6459f0f60..bb1f9f23b 100644
--- a/dart/packages/fory/test/unsigned_serializer_test.dart
+++ b/dart/packages/fory/test/unsigned_serializer_test.dart
@@ -33,58 +33,58 @@ final Uint64 _uint64Max = Uint64.parseHex('ffffffffffffffff');
class UnsignedFields {
UnsignedFields();
- @Uint8Type()
+ @ForyField(type: Uint8Type())
int u8 = 0;
- @Uint16Type()
+ @ForyField(type: Uint16Type())
int u16 = 0;
- @Uint32Type(compress: true)
+ @ForyField(type: Uint32Type(encoding: Encoding.varint))
int u32Var = 0;
- @Uint32Type(compress: false)
+ @ForyField(type: Uint32Type(encoding: Encoding.fixed))
int u32Fixed = 0;
- @Uint64Type(encoding: LongEncoding.varint)
+ @ForyField(type: Uint64Type(encoding: Encoding.varint))
Uint64 u64Var = Uint64(0);
- @Uint64Type(encoding: LongEncoding.fixed)
+ @ForyField(type: Uint64Type(encoding: Encoding.fixed))
Uint64 u64Fixed = Uint64(0);
- @Uint64Type(encoding: LongEncoding.tagged)
+ @ForyField(type: Uint64Type(encoding: Encoding.tagged))
Uint64 u64Tagged = Uint64(0);
- @Uint64Type(encoding: LongEncoding.varint)
+ @ForyField(type: Uint64Type(encoding: Encoding.varint))
int u64VarInt = 0;
- @Uint64Type(encoding: LongEncoding.fixed)
+ @ForyField(type: Uint64Type(encoding: Encoding.fixed))
int u64FixedInt = 0;
- @Uint64Type(encoding: LongEncoding.tagged)
+ @ForyField(type: Uint64Type(encoding: Encoding.tagged))
int u64TaggedInt = 0;
- @Uint8Type()
+ @ForyField(type: Uint8Type())
int? u8Nullable;
- @Uint16Type()
+ @ForyField(type: Uint16Type())
int? u16Nullable;
- @Uint32Type(compress: true)
+ @ForyField(type: Uint32Type(encoding: Encoding.varint))
int? u32VarNullable;
- @Uint32Type(compress: false)
+ @ForyField(type: Uint32Type(encoding: Encoding.fixed))
int? u32FixedNullable;
- @Uint64Type(encoding: LongEncoding.varint)
+ @ForyField(type: Uint64Type(encoding: Encoding.varint))
Uint64? u64VarNullable;
- @Uint64Type(encoding: LongEncoding.fixed)
+ @ForyField(type: Uint64Type(encoding: Encoding.fixed))
Uint64? u64FixedNullable;
- @Uint64Type(encoding: LongEncoding.tagged)
+ @ForyField(type: Uint64Type(encoding: Encoding.tagged))
Uint64? u64TaggedNullable;
- @Uint64Type(encoding: LongEncoding.varint)
+ @ForyField(type: Uint64Type(encoding: Encoding.varint))
int? u64VarIntNullable;
}
@@ -92,7 +92,7 @@ class UnsignedFields {
class UnsignedMetadataReader {
UnsignedMetadataReader();
- @Uint8Type()
+ @ForyField(type: Uint8Type())
int u8 = 0;
int extra = 42;
@@ -102,13 +102,13 @@ class UnsignedMetadataReader {
class UnsignedIntFieldsReader {
UnsignedIntFieldsReader();
- @Uint64Type(encoding: LongEncoding.varint)
+ @ForyField(type: Uint64Type(encoding: Encoding.varint))
int u64Var = 0;
- @Uint64Type(encoding: LongEncoding.fixed)
+ @ForyField(type: Uint64Type(encoding: Encoding.fixed))
int u64Fixed = 0;
- @Uint64Type(encoding: LongEncoding.tagged)
+ @ForyField(type: Uint64Type(encoding: Encoding.tagged))
int u64Tagged = 0;
}
@@ -116,13 +116,13 @@ class UnsignedIntFieldsReader {
class UnsignedWrapperFields {
UnsignedWrapperFields();
- @Uint64Type(encoding: LongEncoding.varint)
+ @ForyField(type: Uint64Type(encoding: Encoding.varint))
Uint64 u64Var = Uint64(0);
- @Uint64Type(encoding: LongEncoding.fixed)
+ @ForyField(type: Uint64Type(encoding: Encoding.fixed))
Uint64 u64Fixed = Uint64(0);
- @Uint64Type(encoding: LongEncoding.tagged)
+ @ForyField(type: Uint64Type(encoding: Encoding.tagged))
Uint64 u64Tagged = Uint64(0);
}
@@ -130,13 +130,13 @@ class UnsignedWrapperFields {
class UnsignedWrapperAsIntFields {
UnsignedWrapperAsIntFields();
- @Uint64Type(encoding: LongEncoding.varint)
+ @ForyField(type: Uint64Type(encoding: Encoding.varint))
int u64Var = 0;
- @Uint64Type(encoding: LongEncoding.fixed)
+ @ForyField(type: Uint64Type(encoding: Encoding.fixed))
int u64Fixed = 0;
- @Uint64Type(encoding: LongEncoding.tagged)
+ @ForyField(type: Uint64Type(encoding: Encoding.tagged))
int u64Tagged = 0;
}
@@ -422,7 +422,9 @@ void main() {
expect(_remoteFieldTypeId(roundTrip, 'u8'), equals(TypeIds.uint8));
expect(_remoteFieldTypeId(roundTrip, 'u16'), equals(TypeIds.uint16));
expect(
- _remoteFieldTypeId(roundTrip, 'u32_var'), equals(TypeIds.varUint32));
+ _remoteFieldTypeId(roundTrip, 'u32_var'),
+ equals(TypeIds.varUint32),
+ );
expect(
_remoteFieldTypeId(roundTrip, 'u32_fixed'), equals(TypeIds.uint32));
expect(
@@ -458,6 +460,39 @@ void main() {
expect(_remoteFieldNullable(roundTrip, 'u64_var_int_nullable'), isTrue);
});
+ test('rejects out-of-range annotated unsigned fields', () {
+ final cases = <({String name, UnsignedFields value})>[
+ (
+ name: 'u8',
+ value: _smallUnsignedFields()..u8 = 0x100,
+ ),
+ (
+ name: 'u16',
+ value: _smallUnsignedFields()..u16 = 0x10000,
+ ),
+ (
+ name: 'u32Var',
+ value: _smallUnsignedFields()..u32Var = -1,
+ ),
+ (
+ name: 'u32FixedNullable',
+ value: _smallUnsignedFields()..u32FixedNullable = 0x100000000,
+ ),
+ ];
+
+ for (final testCase in cases) {
+ for (final compatible in <bool>[false, true]) {
+ final fory = Fory(compatible: compatible);
+ _registerUnsignedFields(fory);
+ expect(
+ () => fory.serialize(testCase.value),
+ throwsA(isA<RangeError>()),
+ reason: '${testCase.name}, compatible=$compatible',
+ );
+ }
+ }
+ });
+
test('web rejects JS-unsafe uint64 Dart int fields', () {
final cases = <({String name, UnsignedFields value})>[
(
diff --git a/dart/packages/fory/test/xlang_protocol_test.dart b/dart/packages/fory/test/xlang_protocol_test.dart
index 1c34bc856..be3b9bb76 100644
--- a/dart/packages/fory/test/xlang_protocol_test.dart
+++ b/dart/packages/fory/test/xlang_protocol_test.dart
@@ -51,14 +51,17 @@ void main() {
test('deserializes BFLOAT16 and BFLOAT16_ARRAY wire values', () {
final fory = Fory();
- final scalarBytes = Uint8List.fromList(fory.serialize(Uint16(0xbf60)));
- scalarBytes[2] = TypeIds.bfloat16;
+ final scalarBytes =
+ Uint8List.fromList(fory.serialize(Bfloat16.fromBits(0xbf60)));
final arrayBytes = Uint8List.fromList(
fory.serialize(
- Uint16List.fromList(<int>[0x3f80, 0xbf80, 0x7fc1]),
+ Bfloat16List.fromList(<Bfloat16>[
+ Bfloat16.fromBits(0x3f80),
+ Bfloat16.fromBits(0xbf80),
+ Bfloat16.fromBits(0x7fc1),
+ ]),
),
);
- arrayBytes[2] = TypeIds.bfloat16Array;
expect(fory.deserialize<Bfloat16>(scalarBytes).toBits(), equals(0xbf60));
expect(
@@ -70,6 +73,16 @@ void main() {
);
});
+ test('serializes root builtins with an explicit wire type', () {
+ final fory = Fory();
+ final bytes = fory.serializeBuiltin(7, wireTypeId: TypeIds.varInt32);
+
+ expect(bytes[0], equals(0x02));
+ expect(bytes[1], equals(0xff));
+ expect(bytes[2], equals(TypeIds.varInt32));
+ expect(fory.deserialize<int>(bytes), equals(7));
+ });
+
test('rejects out-of-band xlang payload headers', () {
final fory = Fory();
final bytes = Uint8List.fromList(fory.serialize('value'));
diff --git a/dart/test/workspace_smoke_test.dart b/dart/test/workspace_smoke_test.dart
index b6e3ee02b..1cb84607d 100644
--- a/dart/test/workspace_smoke_test.dart
+++ b/dart/test/workspace_smoke_test.dart
@@ -23,8 +23,8 @@ import 'package:test/test.dart';
void main() {
test('workspace root can use the rewritten fory package', () {
final fory = Fory();
- final bytes = fory.serialize(Int32(42));
- final roundTrip = fory.deserialize<Int32>(bytes);
- expect(roundTrip, equals(Int32(42)));
+ final bytes = fory.serialize(42);
+ final roundTrip = fory.deserialize<int>(bytes);
+ expect(roundTrip, equals(42));
});
}
diff --git a/docs/compiler/generated-code.md b/docs/compiler/generated-code.md
index a517f4f95..3462ba02d 100644
--- a/docs/compiler/generated-code.md
+++ b/docs/compiler/generated-code.md
@@ -889,8 +889,8 @@ final class Person {
@ForyField(id: 1)
String name = '';
- @ForyField(id: 2)
- Int32 id = Int32(0);
+ @ForyField(id: 2, type: Int32Type())
+ int id = 0;
@ForyField(id: 7)
List<Person_PhoneNumber> phones = <Person_PhoneNumber>[];
@@ -956,14 +956,14 @@ Nested types use flat underscore naming (e.g., `Person_PhoneNumber`, `Person_Pho
Non-optional, non-ref lists of primitive types use typed arrays for zero-copy performance (e.g., `list<int32>` → `Int32List`).
-Reference tracking on list elements or map values uses `@ListType` / `@MapType` annotations:
+Reference tracking on list elements or map values uses the container sugar annotations:
```dart
-@ListType(element: ValueType.ref())
+@ListField(element: DeclaredType(ref: true))
@ForyField(id: 3)
List<Node> children = <Node>[];
-@MapType(value: ValueType.ref())
+@MapField(value: DeclaredType(ref: true))
@ForyField(id: 2)
Map<String, Node> byName = <String, Node>{};
```
@@ -1007,7 +1007,7 @@ void main() {
final person = Person()
..name = 'Alice'
- ..id = Int32(1);
+ ..id = 1;
final bytes = fory.serialize(person);
final roundTrip = fory.deserialize<Person>(bytes);
diff --git a/docs/compiler/index.md b/docs/compiler/index.md
index 7790961ec..605a4e609 100644
--- a/docs/compiler/index.md
+++ b/docs/compiler/index.md
@@ -198,7 +198,7 @@ Fory IDL types map to native types in each language:
| Fory IDL Type | Java | Python | Go | Rust | C++ | C# | JavaScript | Swift | Dart |
| ------------- | --------- | -------------- | -------- | -------- | ------------- | -------- | ---------- | -------- | -------- |
-| `int32` | `int` | `pyfory.int32` | `int32` | `i32` | `int32_t` | `int` | `number` | `Int32` | `Int32` |
+| `int32` | `int` | `pyfory.int32` | `int32` | `i32` | `int32_t` | `int` | `number` | `Int32` | `int` |
| `string` | `String` | `str` | `string` | `String` | `std::string` | `string` | `string` | `String` | `String` |
| `bool` | `boolean` | `bool` | `bool` | `bool` | `bool` | `bool` | `boolean` | `Bool` | `bool` |
diff --git a/docs/compiler/schema-idl.md b/docs/compiler/schema-idl.md
index 9f514fd0d..315fc97e9 100644
--- a/docs/compiler/schema-idl.md
+++ b/docs/compiler/schema-idl.md
@@ -969,12 +969,12 @@ apply to elements. `repeated` is accepted as an alias for `list`.
**List modifier mapping:**
-| Fory IDL | Java | Python | Go | Rust | C++ | Dart |
-| ----------------------- | ---------------------------------------------- | --------------------------------------- | ----------------------- | --------------------- | ----------------------------------------- | ---------------------------------------------------- |
-| `optional list<string>` | `List<String>` + `@ForyField(nullable = true)` | `Optional[List[str]]` | `[]string` + `nullable` | `Option<Vec<String>>` | `std::optional<std::vector<std::string>>` | `List<String>?` |
-| `list<optional string>` | `List<String>` (nullable elements) | `List[Optional[str]]` | `[]*string` | `Vec<Option<String>>` | `std::vector<std::optional<std::string>>` | `List<String?>` |
-| `ref list<User>` | `List<User>` + `@ForyField(ref = true)` | `List[User]` + `pyfory.field(ref=True)` | `[]User` + `ref` | `Arc<Vec<User>>` | `std::shared_ptr<std::vector<User>>` | `List<User>` + `@ForyField(ref: true)` |
-| `list<ref User>` | `List<User>` | `List[User]` | `[]*User` + `ref=false` | `Vec<Arc<User>>` | `std::vector<std::shared_ptr<User>>` | `List<User>` + `@ListType(element: ValueType.ref())` |
+| Fory IDL | Java | Python | Go | Rust | C++ | Dart |
+| ----------------------- | ---------------------------------------------- | --------------------------------------- | ----------------------- | --------------------- | ----------------------------------------- | ------------------------------------------------------------- |
+| `optional list<string>` | `List<String>` + `@ForyField(nullable = true)` | `Optional[List[str]]` | `[]string` + `nullable` | `Option<Vec<String>>` | `std::optional<std::vector<std::string>>` | `List<String>?` |
+| `list<optional string>` | `List<String>` (nullable elements) | `List[Optional[str]]` | `[]*string` | `Vec<Option<String>>` | `std::vector<std::optional<std::string>>` | `List<String?>` |
+| `ref list<User>` | `List<User>` + `@ForyField(ref = true)` | `List[User]` + `pyfory.field(ref=True)` | `[]User` + `ref` | `Arc<Vec<User>>` | `std::shared_ptr<std::vector<User>>` | `List<User>` + `@ForyField(ref: true)` |
+| `list<ref User>` | `List<User>` | `List[User]` | `[]*User` + `ref=false` | `Vec<Arc<User>>` | `std::vector<std::shared_ptr<User>>` | `List<User>` + `@ListField(element: DeclaredType(ref: true))` |
Use `ref(thread_safe=false)` in Fory IDL (or `[(fory).thread_safe_pointer = false]` in protobuf)
to generate `Rc` instead of `Arc` in Rust.
@@ -1062,10 +1062,10 @@ Fory IDL provides fixed-width signed integers (varint encoding for 32/64-bit by
| Fory IDL | Java | Python | Go | Rust | C++ | JavaScript | Dart |
| -------- | ------- | -------------- | ------- | ----- | --------- | ------------------ | ------- |
-| `int8` | `byte` | `pyfory.int8` | `int8` | `i8` | `int8_t` | `number` | `Int8` |
-| `int16` | `short` | `pyfory.int16` | `int16` | `i16` | `int16_t` | `number` | `Int16` |
-| `int32` | `int` | `pyfory.int32` | `int32` | `i32` | `int32_t` | `number` | `Int32` |
-| `int64` | `long` | `pyfory.int64` | `int64` | `i64` | `int64_t` | `bigint \| number` | `int` |
+| `int8` | `byte` | `pyfory.int8` | `int8` | `i8` | `int8_t` | `number` | `int` |
+| `int16` | `short` | `pyfory.int16` | `int16` | `i16` | `int16_t` | `number` | `int` |
+| `int32` | `int` | `pyfory.int32` | `int32` | `i32` | `int32_t` | `number` | `int` |
+| `int64` | `long` | `pyfory.int64` | `int64` | `i64` | `int64_t` | `bigint \| number` | `Int64` |
Fory IDL provides fixed-width unsigned integers (varint encoding for 32/64-bit by default):
@@ -1080,10 +1080,10 @@ Fory IDL provides fixed-width unsigned integers (varint encoding for 32/64-bit b
| Fory IDL | Java | Python | Go | Rust | C++ | JavaScript | Dart |
| -------- | ------- | --------------- | -------- | ----- | ---------- | ------------------ | -------- |
-| `uint8` | `short` | `pyfory.uint8` | `uint8` | `u8` | `uint8_t` | `number` | `UInt8` |
-| `uint16` | `int` | `pyfory.uint16` | `uint16` | `u16` | `uint16_t` | `number` | `UInt16` |
-| `uint32` | `long` | `pyfory.uint32` | `uint32` | `u32` | `uint32_t` | `number` | `UInt32` |
-| `uint64` | `long` | `pyfory.uint64` | `uint64` | `u64` | `uint64_t` | `bigint \| number` | `int` |
+| `uint8` | `short` | `pyfory.uint8` | `uint8` | `u8` | `uint8_t` | `number` | `int` |
+| `uint16` | `int` | `pyfory.uint16` | `uint16` | `u16` | `uint16_t` | `number` | `int` |
+| `uint32` | `long` | `pyfory.uint32` | `uint32` | `u32` | `uint32_t` | `number` | `int` |
+| `uint64` | `long` | `pyfory.uint64` | `uint64` | `u64` | `uint64_t` | `bigint \| number` | `Uint64` |
#### Integer Encoding Variants
@@ -1257,10 +1257,10 @@ message Config {
**Language Mapping:**
-| Fory IDL | Java | Python | Go | Rust | C++ | JavaScript | Dart |
-| -------------------- | ---------------------- | ----------------- | ------------------ | ----------------------- | -------------------------------- | --------------------- | -------------------- |
-| `map<string, int32>` | `Map<String, Integer>` | `Dict[str, int]` | `map[string]int32` | `HashMap<String, i32>` | `std::map<std::string, int32_t>` | `Map<string, number>` | `Map<String, Int32>` |
-| `map<string, User>` | `Map<String, User>` | `Dict[str, User]` | `map[string]User` | `HashMap<String, User>` | `std::map<std::string, User>` | `Map<string, User>` | `Map<String, User>` |
+| Fory IDL | Java | Python | Go | Rust | C++ | JavaScript | Dart |
+| -------------------- | ---------------------- | ----------------- | ------------------ | ----------------------- | -------------------------------- | --------------------- | ------------------- |
+| `map<string, int32>` | `Map<String, Integer>` | `Dict[str, int]` | `map[string]int32` | `HashMap<String, i32>` | `std::map<std::string, int32_t>` | `Map<string, number>` | `Map<String, int>` |
+| `map<string, User>` | `Map<String, User>` | `Dict[str, User]` | `map[string]User` | `HashMap<String, User>` | `std::map<std::string, User>` | `Map<string, User>` | `Map<String, User>` |
**Key Type Restrictions:**
diff --git a/docs/guide/dart/basic-serialization.md b/docs/guide/dart/basic-serialization.md
index 1f261937e..87ea752df 100644
--- a/docs/guide/dart/basic-serialization.md
+++ b/docs/guide/dart/basic-serialization.md
@@ -43,7 +43,9 @@ class Person {
Person();
String name = '';
- Int32 age = Int32(0);
+
+ @ForyField(type: Int32Type())
+ int age = 0;
}
void main() {
@@ -57,7 +59,7 @@ void main() {
final person = Person()
..name = 'Ada'
- ..age = Int32(36);
+ ..age = 36;
final bytes = fory.serialize(person);
final roundTrip = fory.deserialize<Person>(bytes);
@@ -85,7 +87,7 @@ You can serialize collection values directly:
final fory = Fory();
final bytes = fory.serialize(<Object?>[
'hello',
- Int32(42),
+ 42,
true,
]);
final value = fory.deserialize<List<Object?>>(bytes);
@@ -117,8 +119,8 @@ If you want to avoid allocating a new `Uint8List` on every call, use `serializeT
final fory = Fory();
final buffer = Buffer();
-fory.serializeTo(Int32(42), buffer);
-final value = fory.deserializeFrom<Int32>(buffer);
+fory.serializeTo('Ada', buffer);
+final value = fory.deserializeFrom<String>(buffer);
```
This is an optimization. For most applications the default `serialize`/`deserialize` pair is fine.
diff --git a/docs/guide/dart/code-generation.md b/docs/guide/dart/code-generation.md
index 495a6dd89..d24d8c229 100644
--- a/docs/guide/dart/code-generation.md
+++ b/docs/guide/dart/code-generation.md
@@ -43,7 +43,9 @@ class User {
User();
String name = '';
- Int32 age = Int32(0);
+
+ @ForyField(type: Int32Type())
+ int age = 0;
Address address = Address();
}
```
diff --git a/docs/guide/dart/cross-language.md b/docs/guide/dart/cross-language.md
index ed843d3d3..f5d7b5476 100644
--- a/docs/guide/dart/cross-language.md
+++ b/docs/guide/dart/cross-language.md
@@ -74,14 +74,16 @@ class Person {
Person();
String name = '';
- Int32 age = Int32(0);
+
+ @ForyField(type: Int32Type())
+ int age = 0;
}
final fory = Fory();
PersonFory.register(fory, Person, id: 100);
final bytes = fory.serialize(Person()
..name = 'Alice'
- ..age = Int32(30));
+ ..age = 30);
```
### Java
@@ -104,7 +106,7 @@ final fory = Fory(compatible: true);
PersonFory.register(fory, Person, id: 100);
final bytes = fory.serialize(Person()
..name = 'Alice'
- ..age = Int32(30));
+ ..age = 30);
```
### CSharp
@@ -134,7 +136,7 @@ final fory = Fory();
PersonFory.register(fory, Person, id: 100);
final bytes = fory.serialize(Person()
..name = 'Alice'
- ..age = Int32(30));
+ ..age = 30);
```
### Go
@@ -159,16 +161,18 @@ Fory matches fields by name or by stable field ID. For robust cross-language int
1. Use the same type identity on every side (same numeric ID or same `namespace + typeName`).
2. Assign stable `@ForyField(id: ...)` values to all fields before shipping the first payload.
3. Keep field names consistent or rely on IDs, since Dart typically uses `lowerCamelCase` while Go uses `PascalCase` for exported fields and C# often uses `PascalCase` properties.
-4. Use compatible numeric types: `Int32` in Dart for Java `int`, Go `int32`, and C# `int`; `double` in Dart for 64-bit floats; `Float32` for 32-bit.
+4. Use explicit numeric field metadata: `@ForyField(type: Int32Type())` in Dart for Java `int`, Go `int32`, and C# `int`; `double` in Dart for 64-bit floats; `Float32` for 32-bit; `Int64` / `Uint64` for full-range 64-bit values.
5. Use `Timestamp`, `LocalDate`, and `Duration` for temporal fields rather than raw `DateTime`.
6. Validate real round trips across all languages before shipping.
## Type Mapping Notes for Dart
-Because Dart `int` is not itself a promise about the exact xlang wire width, prefer wrappers or numeric field annotations when exact cross-language interpretation matters:
+Because Dart `int` is not itself a promise about the exact xlang wire width, prefer explicit field metadata when exact cross-language interpretation matters:
-- `Int32` for xlang `int32`
-- `Uint32` for xlang `uint32`
+- `@ForyField(type: Int32Type())` for xlang `int32`
+- `@ForyField(type: Uint32Type())` for xlang `uint32`
+- `@ForyField(type: Int8Type())` / `@ForyField(type: Int16Type())` / `@ForyField(type: Uint8Type())` / `@ForyField(type: Uint16Type())` for narrower integer widths
+- `Int64` and `Uint64` for full-range 64-bit values on web
- `Float16`, `Bfloat16`, and `Float32` for reduced-width floating point
- `Float16List` and `Bfloat16List` for 16-bit floating-point array payloads
- `Timestamp`, `LocalDate`, and `Duration` for explicit temporal semantics
diff --git a/docs/guide/dart/field-configuration.md b/docs/guide/dart/field-configuration.md
index 5e0b439f0..a7d02904b 100644
--- a/docs/guide/dart/field-configuration.md
+++ b/docs/guide/dart/field-configuration.md
@@ -88,32 +88,47 @@ Controls whether Fory writes the concrete runtime type of the field value into t
Object? payload; // can hold any registered type at runtime
```
-## Numeric Field Annotations
+## Numeric Field Types
-Dart `int` is a 64-bit value at runtime. When exchanging messages with Java, Go, or C#, the receiving side may expect a narrower integer. Use a numeric annotation to pin the exact wire format:
+Dart `int` is a 64-bit value at runtime. When exchanging messages with Java, Go, or C#, the receiving side may expect a narrower integer. Use `@ForyField(type: ...)` to pin the exact wire format:
```dart
@ForyStruct()
class Sample {
Sample();
- @Int32Type(compress: false) // always writes 4 bytes
+ @ForyField(type: Int32Type(encoding: Encoding.fixed))
int fixedWidthInt = 0;
- @Int64Type(encoding: LongEncoding.tagged) // variable-length encoding
- int compactLong = 0;
+ @ForyField(type: Int64Type(encoding: Encoding.tagged))
+ Int64 compactLong = Int64(0);
- @Uint32Type(compress: true) // variable-length unsigned
+ @ForyField(type: Uint32Type())
int smallUnsigned = 0;
}
```
-Available annotations: `@Int32Type`, `@Int64Type`, `@Uint8Type`, `@Uint16Type`, `@Uint32Type`, `@Uint64Type`.
+Available scalar type nodes include `Int8Type`, `Int16Type`, `Int32Type`,
+`Int64Type`, `Uint8Type`, `Uint16Type`, `Uint32Type`, `Uint64Type`,
+`Float16Type`, `Bfloat16Type`, and `Float32Type`.
-Alternatively, use explicit wrapper types such as `Int32`, `Int64`, `Uint32`,
-and `Uint64` as described in [Supported Types](supported-types.md). Wrappers use
-compact varint encodings by default; use annotations or generated field
-metadata when a fixed-width or tagged encoding is required.
+For nested containers, use `ListField`, `SetField`, `MapField`, or a full
+`ForyField(type: ...)` tree:
+
+```dart
+@MapField(
+ value: ListType(
+ element: Int32Type(encoding: Encoding.fixed),
+ ),
+)
+Map<String, List<int?>> metrics = <String, List<int?>>{};
+```
+
+Generic `List<int>` still uses the `list` wire type even with primitive element
+specs. Packed `*_array` wire kinds come from dedicated carriers such as
+`Int32List`, `Uint32List`, `Int64List`, and `Uint64List`. If you annotate a
+generic `List<int>` with a non-null fixed-width primitive element spec, code
+generation rejects it and tells you to use the matching typed list carrier.
## Aligning Fields Across Languages
diff --git a/docs/guide/dart/index.md b/docs/guide/dart/index.md
index cab137c48..34e1da64e 100644
--- a/docs/guide/dart/index.md
+++ b/docs/guide/dart/index.md
@@ -68,7 +68,9 @@ class Person {
Person();
String name = '';
- Int32 age = Int32(0);
+
+ @ForyField(type: Int32Type())
+ int age = 0;
Color favoriteColor = Color.red;
List<String> tags = <String>[];
}
@@ -90,7 +92,7 @@ void main() {
final person = Person()
..name = 'Ada'
- ..age = Int32(36)
+ ..age = 36
..favoriteColor = Color.blue
..tags = <String>['engineer', 'mathematician'];
@@ -114,8 +116,9 @@ dart run build_runner build --delete-conflicting-outputs
- `fory.serialize(value)` — returns `Uint8List` bytes
- `fory.deserialize<T>(bytes)` — returns a `T`
- `@ForyStruct()` — marks a class for code generation
-- `@ForyField(...)` — per-field options (skip, ID, nullability, references)
-- Integer wrappers: `Int8`, `Int16`, `Int32`, `Int64`, `Uint8`, `Uint16`, `Uint32`, `Uint64`
+- `@ForyField(...)` — per-field options and canonical `type:` overrides
+- `@ListField(...)`, `@SetField(...)`, `@MapField(...)` — container sugar for nested `type:` trees
+- Exact-value wrappers: `Int64`, `Uint64`, `Float16`, `Bfloat16`, `Float32`
- Float wrappers: `Float16`, `Bfloat16`, `Float32`
- 16-bit float arrays: `Float16List`, `Bfloat16List`
- Time types: `LocalDate`, `Timestamp`, `Duration`
diff --git a/docs/guide/dart/schema-evolution.md b/docs/guide/dart/schema-evolution.md
index 8d33297a9..1afbbdeb4 100644
--- a/docs/guide/dart/schema-evolution.md
+++ b/docs/guide/dart/schema-evolution.md
@@ -71,7 +71,7 @@ If you add field IDs after payloads are already in production, existing stored m
**Unsafe changes** (may break existing messages):
- Reuse an existing field ID for a different field.
-- Change a field's type to an incompatible type (e.g., `Int32` → `String`).
+- Change a field's type to an incompatible type (e.g., `@ForyField(type: Int32Type()) int` → `String`).
- Change the registration identity (`id`, `namespace`, or `typeName`) of a type after messages are in production.
- Change a field's logical meaning without changing its ID.
diff --git a/docs/guide/dart/supported-types.md b/docs/guide/dart/supported-types.md
index e7ee82e73..4b43d10e8 100644
--- a/docs/guide/dart/supported-types.md
+++ b/docs/guide/dart/supported-types.md
@@ -25,17 +25,17 @@ This page lists the Dart types you can use in Fory messages, and flags where you
The following Dart types serialize directly without any special handling:
-| Dart type | Cross-language notes |
-| -------------------- | ----------------------------------------------------------------------------------------------------------- |
-| `bool` | Direct mapping |
-| `int` | Serialized as 64-bit by default. Use wrappers or `@Int32Type` etc. when the peer expects a narrower integer |
-| `double` | Maps to 64-bit float. Use `Float32` wrapper when the peer expects 32-bit |
-| `String` | Direct mapping |
-| `Uint8List` | Binary blob |
-| `List`, `Set`, `Map` | Supported; element types must also be supported |
-| `DateTime` | Use `Timestamp` or `LocalDate` wrappers for explicit semantics |
-
-## Integer Wrappers
+| Dart type | Cross-language notes |
+| -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `bool` | Direct mapping |
+| `int` | Serialized as 64-bit by default. Use `@ForyField(type: Int8Type/Int16Type/Int32Type/Uint8Type/Uint16Type/Uint32Type)` when the peer expects a narrower integer |
+| `double` | Maps to 64-bit float. Use `Float32` wrapper when the peer expects 32-bit |
+| `String` | Direct mapping |
+| `Uint8List` | Binary blob |
+| `List`, `Set`, `Map` | Supported; element types must also be supported |
+| `DateTime` | Use `Timestamp` or `LocalDate` wrappers for explicit semantics |
+
+## Integer Fields
Dart VM/native `int` can represent signed 64-bit values, while Dart web `int`
is limited to JavaScript-safe integer precision. If the peer language expects a
@@ -43,23 +43,31 @@ is limited to JavaScript-safe integer precision. If the peer language expects a
the deserialization may fail or silently truncate. For browser and Flutter web
precision rules, see [Web Platform Support](web-platform-support.md).
-Use an integer wrapper or field annotation to select the wire type explicitly:
+Use field metadata to select the wire type explicitly for 8/16/32-bit fields:
```dart
-final Int8 tiny = Int8(-1); // 8-bit signed
-final Int16 shortValue = Int16(7); // 16-bit signed
-final Int32 age = Int32(36); // 32-bit signed, varint by default
-final Int64 seq = Int64(0); // signed 64-bit, varint by default
-final Uint8 flags = Uint8(255); // 8-bit unsigned
-final Uint16 port = Uint16(65535); // 16-bit unsigned
-final Uint32 count = Uint32(4000000000); // 32-bit unsigned, varint by default
-final Uint64 offset = Uint64(0); // unsigned 64-bit, varint by default
+@ForyStruct()
+class Metrics {
+ Metrics();
+
+ @ForyField(type: Int8Type())
+ int tiny = 0;
+
+ @ForyField(type: Int32Type(encoding: Encoding.fixed))
+ int age = 0;
+
+ @ForyField(type: Uint32Type())
+ int count = 0;
+
+ Int64 sequence = Int64(0);
+ Uint64 offset = Uint64(0);
+}
```
-Each wrapper clamps or normalizes the stored value to the target bit width.
-Root `Int32`, `Int64`, `Uint32`, and `Uint64` values use compact varint wire
-types by default. Use `@Int64Type`, `@Uint32Type`, `@Uint64Type`, or generated
-field metadata when a fixed-width or tagged encoding is required.
+Generated serializers range-check annotated `int` values before writing them.
+Use `Int64` and `Uint64` when you need full-range 64-bit values, especially on
+web. A plain root `int` value serializes as xlang `int64`; exact 8/16/32-bit
+wire widths are selected through field metadata or low-level `Buffer` APIs.
On Dart VM, `Int64` and `Uint64` are extension types over `int`. Once a value is
passed through an `Object`-typed dynamic/root boundary, the VM cannot recover
@@ -115,7 +123,9 @@ class User {
User();
String name = '';
- Int32 age = Int32(0); // use Int32 when peers expect a 32-bit integer
+
+ @ForyField(type: Int32Type())
+ int age = 0; // use explicit field metadata when peers expect a 32-bit integer
}
```
@@ -123,11 +133,22 @@ See [Code Generation](code-generation.md).
## Collections
-Fory supports `List<T>`, `Set<T>`, and `Map<K, V>`. Element and key types must also be serializable types. Avoid using mutable objects as map keys.
+Fory supports `List<T>`, `Set<T>`, and `Map<K, V>`. Element and key types must
+also be serializable types. Avoid using mutable objects as map keys.
+
+Generic `List<int>` with primitive element metadata still uses the `list` wire
+type. Dedicated array wire kinds come from dedicated carriers:
+
+- `List<bool>` for `bool_array`
+- `Int8List`, `Int16List`, `Int32List`, `Int64List`
+- `Uint8List`, `Uint16List`, `Uint32List`, `Uint64List`
+- `Float16List`, `Bfloat16List`, `Float32List`, `Float64List`
## Compatibility Tip
-When in doubt about whether a Dart type will match what the peer expects, use the explicit wrapper types. Guessing the wrong numeric width is one of the most common cross-language bugs.
+When in doubt about whether a Dart type will match what the peer expects, make
+the width explicit with `@ForyField(type: ...)`. Guessing the wrong numeric
+width is one of the most common cross-language bugs.
## Related Topics
diff --git a/docs/guide/dart/troubleshooting.md b/docs/guide/dart/troubleshooting.md
index 43df08e61..3938e95a5 100644
--- a/docs/guide/dart/troubleshooting.md
+++ b/docs/guide/dart/troubleshooting.md
@@ -74,7 +74,7 @@ Checklist:
1. Same registration identity on both sides (same numeric ID **or** same `namespace + typeName`).
2. Stable `@ForyField(id: ...)` assigned before the first payload was produced.
-3. Compatible numeric widths — use `Int32` in Dart when the peer field is `int` (Java), `int32` (Go), or `int` (C#).
+3. Compatible numeric widths — use `@ForyField(type: Int32Type())` in Dart when the peer field is `int` (Java), `int32` (Go), or `int` (C#).
4. `Timestamp` / `LocalDate` instead of raw `DateTime` for date/time fields.
5. `compatible: true` on **both** sides if using schema evolution.
@@ -114,9 +114,9 @@ class FileBlock {
}
```
-`@Int64Type` changes the wire encoding for a Dart `int` field, but it does not
-remove the web integer precision limit. Use `Int64` for full-range signed
-values and `Uint64` for full-range unsigned values. See
+`@ForyField(type: Int64Type(...))` changes the wire encoding for a Dart `int`
+field, but it does not remove the web integer precision limit. Use `Int64` for
+full-range signed values and `Uint64` for full-range unsigned values. See
[Web Platform Support](web-platform-support.md) for the full browser support
matrix and migration guidance.
diff --git a/docs/guide/dart/web-platform-support.md b/docs/guide/dart/web-platform-support.md
index 3ca615426..4cb63da47 100644
--- a/docs/guide/dart/web-platform-support.md
+++ b/docs/guide/dart/web-platform-support.md
@@ -93,21 +93,22 @@ by JavaScript numbers and are precise only in the JS-safe integer range:
Use this rule when choosing field types:
-| Logical value | Recommended Dart field type on web | Notes |
-| ---------------------------------------- | -------------------------------------------------------------------- | -------------------------------------------------------------------- |
-| Signed 64-bit value within JS-safe range | `int` | Works with default `int64` mapping and `@Int64Type` encodings. |
-| Full signed 64-bit range | `Int64` | Preserves values outside the JS-safe range. |
-| Unsigned 64-bit value | `Uint64` | Required for values that do not fit in signed or JS-safe Dart `int`. |
-| 8/16/32-bit integer | `Int8`, `Int16`, `Int32`, `Uint8`, `Uint16`, `Uint32` or annotations | Use wrappers or numeric annotations to match peer runtimes exactly. |
+| Logical value | Recommended Dart field type on web | Notes |
+| ---------------------------------------- | ---------------------------------- | ------------------------------------------------------------------------------------ |
+| Signed 64-bit value within JS-safe range | `int` | Works with default `int64` mapping and `@ForyField(type: Int64Type(...))` encodings. |
+| Full signed 64-bit range | `Int64` | Preserves values outside the JS-safe range. |
+| Unsigned 64-bit value | `Uint64` | Required for values that do not fit in signed or JS-safe Dart `int`. |
+| 8/16/32-bit integer | `int` + `@ForyField(type: ...)` | Use explicit field metadata to match peer runtimes exactly. |
-`@Int64Type` controls the wire encoding of a Dart `int` field:
+`@ForyField(type: Int64Type(...))` controls the wire encoding of a Dart `int`
+field:
```dart
@ForyStruct()
class SafeCounter {
SafeCounter();
- @Int64Type(encoding: LongEncoding.tagged)
+ @ForyField(type: Int64Type(encoding: Encoding.tagged))
int count = 0; // keep web values inside the JS-safe range
}
```
diff --git a/docs/guide/xlang/field-reference-tracking.md b/docs/guide/xlang/field-reference-tracking.md
index 2aafbf2ee..d7bfb4ebf 100644
--- a/docs/guide/xlang/field-reference-tracking.md
+++ b/docs/guide/xlang/field-reference-tracking.md
@@ -132,11 +132,11 @@ public class Document {
String title;
// Enable ref tracking for this field
- @ForyField(trackingRef = true)
+ @ForyField(ref = true)
Author author;
// Shared across documents, track refs to avoid duplicates
- @ForyField(trackingRef = true)
+ @ForyField(ref = true)
List<Tag> tags;
}
```
diff --git a/docs/specification/xlang_serialization_spec.md b/docs/specification/xlang_serialization_spec.md
index 629d26860..c109a6d28 100644
--- a/docs/specification/xlang_serialization_spec.md
+++ b/docs/specification/xlang_serialization_spec.md
@@ -1437,9 +1437,14 @@ Object value layout:
The schema hash is written only when class-version checking is enabled. It is the low 32 bits of a
MurmurHash3 x64_128 of the struct fingerprint string:
-- For each field, build `<field_id_or_name>,<type_id>,<ref>,<nullable>;`.
+- For each field, build `<field_id_or_name>,<field_type_fingerprint>;`.
- Field identifier is the tag ID if present, otherwise the snake_case field name.
- Sort by field identifier lexicographically before concatenation.
+- `field_type_fingerprint` is recursive:
+ - Leaf: `<type_id>,<ref>,<nullable>`
+ - `LIST` / `SET`: `<type_id>,<ref>,<nullable>[<element_fingerprint>]`
+ - `MAP`: `<type_id>,<ref>,<nullable>[<key_fingerprint>|<value_fingerprint>]`
+- Nested container element/key/value fingerprints include nested type ID, container shape, and effective integer encoding, but nested `nullable` and `ref` policy are always hashed as `0`. Only the root field `nullable` and `ref` bits participate in schema hash, because nested reads honor the wire null/ref flags directly.
Field values are serialized in Fory order. Primitive fields are written as raw values (nullable
primitives include a null flag). Non-primitive fields write ref/null flags as needed and then the
diff --git a/go/fory/field_info.go b/go/fory/field_info.go
index c622a5280..ba3e52d72 100644
--- a/go/fory/field_info.go
+++ b/go/fory/field_info.go
@@ -529,6 +529,8 @@ type FieldFingerprintInfo struct {
FieldID int
// FieldName is the snake_case field name (used when FieldID < 0)
FieldName string
+ // TypeSpec is the effective nested field type specification when available.
+ TypeSpec *TypeSpec
// TypeID is the Fory type ID for the field
TypeID TypeId
// Ref is true if reference tracking is enabled for this field
@@ -541,7 +543,8 @@ type FieldFingerprintInfo struct {
//
// Fingerprint Format:
//
-// Each field contributes: "<field_id_or_name>,<type_id>,<ref>,<nullable>;"
+// Each field contributes:
+// "<field_id_or_name>,<type_id>,<ref>,<nullable>[<nested_type_fingerprint>];"
// Fields are sorted by field_id_or_name (lexicographically as strings)
//
// Field Components:
@@ -583,27 +586,80 @@ func ComputeStructFingerprint(fields []FieldFingerprintInfo) string {
// Field identifier
sb.WriteString(fw.sortKey)
sb.WriteString(",")
- // Type ID
- sb.WriteString(fmt.Sprintf("%d", fw.field.TypeID))
- sb.WriteString(",")
- // Ref flag
- if fw.field.Ref {
- sb.WriteString("1")
- } else {
- sb.WriteString("0")
- }
- sb.WriteString(",")
- // Nullable flag
- if fw.field.Nullable {
- sb.WriteString("1")
- } else {
- sb.WriteString("0")
- }
+ appendFieldTypeFingerprint(&sb, fw.field)
sb.WriteString(";")
}
return sb.String()
}
+func appendFieldTypeFingerprint(sb *strings.Builder, field FieldFingerprintInfo) {
+ if field.TypeSpec != nil {
+ appendTypeSpecFingerprint(sb, field.TypeSpec, true, true)
+ return
+ }
+ sb.WriteString(fmt.Sprintf("%d", field.TypeID))
+ sb.WriteString(",")
+ if field.Ref {
+ sb.WriteString("1")
+ } else {
+ sb.WriteString("0")
+ }
+ sb.WriteString(",")
+ if field.Nullable {
+ sb.WriteString("1")
+ } else {
+ sb.WriteString("0")
+ }
+}
+
+func appendTypeSpecFingerprint(sb *strings.Builder, spec *TypeSpec, includeRef bool, includeNullable bool) {
+ if spec == nil {
+ if includeNullable {
+ sb.WriteString(fmt.Sprintf("%d,0,1", UNKNOWN))
+ } else {
+ sb.WriteString(fmt.Sprintf("%d,0,0", UNKNOWN))
+ }
+ return
+ }
+ sb.WriteString(fmt.Sprintf("%d", fingerprintTypeID(spec.TypeId())))
+ sb.WriteString(",")
+ if includeRef && spec.TrackRef {
+ sb.WriteString("1")
+ } else {
+ sb.WriteString("0")
+ }
+ sb.WriteString(",")
+ if includeNullable && spec.Nullable {
+ sb.WriteString("1")
+ } else {
+ sb.WriteString("0")
+ }
+ switch spec.TypeId() {
+ case LIST, SET:
+ sb.WriteString("[")
+ appendTypeSpecFingerprint(sb, spec.Element, false, false)
+ sb.WriteString("]")
+ case MAP:
+ sb.WriteString("[")
+ appendTypeSpecFingerprint(sb, spec.Key, false, false)
+ sb.WriteString("|")
+ appendTypeSpecFingerprint(sb, spec.Value, false, false)
+ sb.WriteString("]")
+ }
+}
+
+func fingerprintTypeID(typeID TypeId) TypeId {
+ switch typeID {
+ case ENUM, NAMED_ENUM,
+ STRUCT, COMPATIBLE_STRUCT, NAMED_STRUCT, NAMED_COMPATIBLE_STRUCT,
+ EXT, NAMED_EXT,
+ UNION, TYPED_UNION, NAMED_UNION:
+ return UNKNOWN
+ default:
+ return typeID
+ }
+}
+
// Field sorting helpers
type triple struct {
diff --git a/go/fory/struct_init.go b/go/fory/struct_init.go
index 95116ef43..9c242339b 100644
--- a/go/fory/struct_init.go
+++ b/go/fory/struct_init.go
@@ -881,7 +881,16 @@ func (s *structSerializer) computeHash() int32 {
fields = append(fields, FieldFingerprintInfo{
FieldID: tagID,
FieldName: SnakeCase(field.Meta.Name),
- TypeID: typeId,
+ TypeSpec: func() *TypeSpec {
+ if field.Meta.Spec == nil || field.Meta.Spec.Type == nil {
+ return nil
+ }
+ projected := field.Meta.Spec.Type.Clone()
+ projected.Nullable = nullable
+ projected.TrackRef = explicitRef
+ return projected
+ }(),
+ TypeID: typeId,
// Ref is based on explicit tag annotation only, NOT runtime ref_tracking config
// This allows fingerprint to be computed at compile time for C++/Rust
Ref: explicitRef,
diff --git a/java/fory-core/src/main/java/org/apache/fory/meta/FieldTypes.java b/java/fory-core/src/main/java/org/apache/fory/meta/FieldTypes.java
index 4f28af89f..83dd1bae6 100644
--- a/java/fory-core/src/main/java/org/apache/fory/meta/FieldTypes.java
+++ b/java/fory-core/src/main/java/org/apache/fory/meta/FieldTypes.java
@@ -84,7 +84,7 @@ public class FieldTypes {
}
/** Build field type from generics, nested generics will be extracted too. */
- static FieldType buildFieldType(TypeResolver resolver, Field field) {
+ public static FieldType buildFieldType(TypeResolver resolver, Field field) {
Preconditions.checkNotNull(field);
GenericType genericType = resolver.buildGenericType(TypeRef.of(field.getAnnotatedType()));
return buildFieldType(resolver, field, genericType);
@@ -299,6 +299,10 @@ public class FieldTypes {
return nullable;
}
+ public int getTypeId() {
+ return typeId;
+ }
+
private int typeKind() {
if (this instanceof RegisteredFieldType) {
return KIND_REGISTERED;
diff --git a/java/fory-core/src/main/java/org/apache/fory/serializer/struct/Fingerprint.java b/java/fory-core/src/main/java/org/apache/fory/serializer/struct/Fingerprint.java
index a7220e313..748dd5ea5 100644
--- a/java/fory-core/src/main/java/org/apache/fory/serializer/struct/Fingerprint.java
+++ b/java/fory-core/src/main/java/org/apache/fory/serializer/struct/Fingerprint.java
@@ -36,6 +36,7 @@ import org.apache.fory.collection.UInt64List;
import org.apache.fory.collection.UInt8List;
import org.apache.fory.logging.Logger;
import org.apache.fory.logging.LoggerFactory;
+import org.apache.fory.meta.FieldTypes;
import org.apache.fory.reflect.ReflectionUtils;
import org.apache.fory.resolver.TypeInfo;
import org.apache.fory.resolver.TypeResolver;
@@ -52,7 +53,8 @@ public class Fingerprint {
*
* <p><b>Fingerprint Format:</b>
*
- * <p>Each field contributes: {@code <field_id_or_name>,<type_id>,<ref>,<nullable>;}
+ * <p>Each field contributes: {@code
+ * <field_id_or_name>,<type_id>,<ref>,<nullable>[<nested_type_fingerprint>];}
*
* <p>Fields are sorted by their identifier (field ID or name) lexicographically as strings.
*
@@ -91,7 +93,12 @@ public class Fingerprint {
List<String[]> fieldInfos = new ArrayList<>(descriptors.size());
for (Descriptor descriptor : descriptors) {
Class<?> rawType = descriptor.getTypeRef().getRawType();
- int typeId = getTypeId(resolver, descriptor);
+ FieldTypes.FieldType fieldType =
+ descriptor.getField() == null
+ ? null
+ : FieldTypes.buildFieldType(resolver, descriptor.getField());
+ int typeId =
+ fieldType != null ? fingerprintTypeId(fieldType) : getTypeId(resolver, descriptor);
// Get field identifier: tag ID if configured, otherwise snake_case name
String fieldIdentifier;
@@ -128,10 +135,12 @@ public class Fingerprint {
nullable = descriptor.isNullable() ? '1' : '0';
}
- fieldInfos.add(
- new String[] {
- fieldIdentifier, String.valueOf(typeId), String.valueOf(ref), String.valueOf(nullable)
- });
+ StringBuilder fieldFingerprint =
+ new StringBuilder().append(typeId).append(',').append(ref).append(',').append(nullable);
+ if (fieldType != null) {
+ appendNestedFingerprint(fieldFingerprint, fieldType);
+ }
+ fieldInfos.add(new String[] {fieldIdentifier, fieldFingerprint.toString()});
}
// Sort by field identifier (lexicographically as strings)
@@ -140,15 +149,7 @@ public class Fingerprint {
// Build fingerprint string
StringBuilder builder = new StringBuilder();
for (String[] info : fieldInfos) {
- builder
- .append(info[0])
- .append(',')
- .append(info[1])
- .append(',')
- .append(info[2])
- .append(',')
- .append(info[3])
- .append(';');
+ builder.append(info[0]).append(',').append(info[1]).append(';');
}
String fingerprint = builder.toString();
if (Utils.DEBUG_OUTPUT_ENABLED) {
@@ -158,6 +159,59 @@ public class Fingerprint {
return fingerprint;
}
+ private static void appendNestedFingerprint(
+ StringBuilder builder, FieldTypes.FieldType fieldType) {
+ if (fieldType instanceof FieldTypes.CollectionFieldType) {
+ FieldTypes.CollectionFieldType collectionFieldType =
+ (FieldTypes.CollectionFieldType) fieldType;
+ builder.append('[');
+ appendFieldTypeFingerprint(builder, collectionFieldType.getElementType(), false, false);
+ builder.append(']');
+ return;
+ }
+ if (fieldType instanceof FieldTypes.MapFieldType) {
+ FieldTypes.MapFieldType mapFieldType = (FieldTypes.MapFieldType) fieldType;
+ builder.append('[');
+ appendFieldTypeFingerprint(builder, mapFieldType.getKeyType(), false, false);
+ builder.append('|');
+ appendFieldTypeFingerprint(builder, mapFieldType.getValueType(), false, false);
+ builder.append(']');
+ }
+ }
+
+ private static void appendFieldTypeFingerprint(
+ StringBuilder builder,
+ FieldTypes.FieldType fieldType,
+ boolean includeRef,
+ boolean includeNullable) {
+ builder
+ .append(fingerprintTypeId(fieldType))
+ .append(',')
+ .append(includeRef && fieldType.trackingRef() ? '1' : '0')
+ .append(',')
+ .append(includeNullable && fieldType.nullable() ? '1' : '0');
+ appendNestedFingerprint(builder, fieldType);
+ }
+
+ private static int fingerprintTypeId(FieldTypes.FieldType fieldType) {
+ int typeId = fieldType.getTypeId();
+ if (typeId == Types.UNKNOWN) {
+ return Types.UNKNOWN;
+ }
+ if (Types.isUnionType(typeId)
+ || typeId == Types.ENUM
+ || typeId == Types.NAMED_ENUM
+ || typeId == Types.STRUCT
+ || typeId == Types.COMPATIBLE_STRUCT
+ || typeId == Types.NAMED_STRUCT
+ || typeId == Types.NAMED_COMPATIBLE_STRUCT
+ || typeId == Types.EXT
+ || typeId == Types.NAMED_EXT) {
+ return Types.UNKNOWN;
+ }
+ return typeId;
+ }
+
private static int getTypeId(TypeResolver resolver, Descriptor descriptor) {
Class<?> cls = descriptor.getTypeRef().getRawType();
Integer primitiveListTypeId = getPrimitiveListTypeId(cls);
diff --git a/java/fory-core/src/test/java/org/apache/fory/xlang/DartXlangTest.java b/java/fory-core/src/test/java/org/apache/fory/xlang/DartXlangTest.java
index 07ab430eb..de36a39da 100644
--- a/java/fory-core/src/test/java/org/apache/fory/xlang/DartXlangTest.java
+++ b/java/fory-core/src/test/java/org/apache/fory/xlang/DartXlangTest.java
@@ -336,4 +336,16 @@ public class DartXlangTest extends XlangTestBase {
public void testUnsignedSchemaCompatible(boolean enableCodegen) throws java.io.IOException {
super.testUnsignedSchemaCompatible(enableCodegen);
}
+
+ @Test(groups = "xlang", dataProvider = "enableCodegenParallel")
+ public void testNestedAnnotatedContainerSchemaConsistent(boolean enableCodegen)
+ throws java.io.IOException {
+ super.testNestedAnnotatedContainerSchemaConsistent(enableCodegen);
+ }
+
+ @Test(groups = "xlang", dataProvider = "enableCodegenParallel")
+ public void testNestedAnnotatedContainerCompatible(boolean enableCodegen)
+ throws java.io.IOException {
+ super.testNestedAnnotatedContainerCompatible(enableCodegen);
+ }
}
diff --git a/javascript/packages/core/lib/meta/TypeMeta.ts b/javascript/packages/core/lib/meta/TypeMeta.ts
index b04d38aa5..ef0770b0b 100644
--- a/javascript/packages/core/lib/meta/TypeMeta.ts
+++ b/javascript/packages/core/lib/meta/TypeMeta.ts
@@ -236,29 +236,43 @@ export class TypeMeta {
computeStructFingerprint(fields: FieldInfo[]) {
let fieldInfos = [];
for (const field of fields) {
- let typeId = field.getTypeId();
- if (TypeId.userDefinedType(typeId)) {
- typeId = TypeId.UNKNOWN;
- }
let fieldIdentifier = "";
if (field.hasFieldId()) {
fieldIdentifier = `${field.getFieldId()}`;
} else {
fieldIdentifier = TypeMeta.toSnakeCase(field.getFieldName());
}
- const ref = field.trackingRef ? "1" : "0";
- const nullable = field.nullable ? "1" : "0";
- fieldInfos.push([fieldIdentifier, `${typeId}`, ref, nullable]);
+ fieldInfos.push([fieldIdentifier, this.computeFieldTypeFingerprint(field, true, true)]);
}
fieldInfos = fieldInfos.sort((a, b) => a[0].localeCompare(b[0]));
let result = "";
for (const fieldInfo of fieldInfos) {
- result += [fieldInfo[0], fieldInfo[1], fieldInfo[2], fieldInfo[3]].join(",");
- result += ";";
+ result += `${fieldInfo[0]},${fieldInfo[1]};`;
+ }
+ return result;
+ }
+
+ private computeFieldTypeFingerprint(field: InnerFieldInfo, includeRef: boolean, includeNullable: boolean) {
+ const ref = includeRef && field.trackingRef ? "1" : "0";
+ const nullable = includeNullable && field.nullable ? "1" : "0";
+ let result = `${this.fingerprintTypeId(field.typeId)},${ref},${nullable}`;
+ if (field.typeId === TypeId.LIST) {
+ result += `[${this.computeFieldTypeFingerprint(field.options!.inner!, false, false)}]`;
+ } else if (field.typeId === TypeId.SET) {
+ result += `[${this.computeFieldTypeFingerprint(field.options!.key!, false, false)}]`;
+ } else if (field.typeId === TypeId.MAP) {
+ result += `[${this.computeFieldTypeFingerprint(field.options!.key!, false, false)}|${this.computeFieldTypeFingerprint(field.options!.value!, false, false)}]`;
}
return result;
}
+ private fingerprintTypeId(typeId: number) {
+ if (TypeId.userDefinedType(typeId) || typeId === TypeId.UNION || typeId === TypeId.TYPED_UNION || typeId === TypeId.NAMED_UNION) {
+ return TypeId.UNKNOWN;
+ }
+ return typeId;
+ }
+
computeStructHash() {
const fields = TypeMeta.groupFieldsByType(this.fields);
const fingerprint = this.computeStructFingerprint(fields);
diff --git a/python/pyfory/struct.py b/python/pyfory/struct.py
index ad4492e82..857a2e3a2 100644
--- a/python/pyfory/struct.py
+++ b/python/pyfory/struct.py
@@ -57,11 +57,14 @@ from pyfory.types import (
)
from pyfory.type_util import (
TypeVisitor,
+ _get_args,
+ _get_origin,
infer_field,
get_homogeneous_tuple_elem_type,
is_subclass,
get_type_hints,
unwrap_optional,
+ unwrap_ref,
)
from pyfory.serialization import Buffer
from pyfory.serialization import ENABLE_FORY_CYTHON_SERIALIZATION
@@ -825,7 +828,7 @@ def compute_struct_fingerprint(type_resolver, field_names, serializers, nullable
Computes the fingerprint string for a struct type used in schema versioning.
Fingerprint Format:
- Each field contributes: <field_id_or_name>,<type_id>,<ref>,<nullable>;
+ Each field contributes: <field_id_or_name>,<type_id>,<ref>,<nullable>[<child...>];
Fields are sorted by tag ID (if >=0) or field name (if id=-1).
Field Components:
@@ -844,7 +847,7 @@ def compute_struct_fingerprint(type_resolver, field_names, serializers, nullable
if nullable_map is None:
nullable_map = {}
- # Build field info list for fingerprint: (sort_key, field_id_or_name, type_id, ref_flag, nullable_flag)
+ # Build field info list for fingerprint: (sort_key, field_id_or_name, type_fingerprint)
fp_fields = []
# Build a lookup for field_infos by name if available
@@ -858,29 +861,16 @@ def compute_struct_fingerprint(type_resolver, field_names, serializers, nullable
# Get field metadata if available
fi = field_info_map.get(field_name)
tag_id = fi.tag_id if fi else -1
- ref_flag = "1" if (fi and fi.ref) else "0"
-
- if serializer is None:
- type_id = TypeId.UNKNOWN
- # For unknown serializers, use nullable from map (defaults to False for xlang)
- nullable_flag = "1" if nullable_map.get(field_name, False) else "0"
- else:
- type_id = type_resolver.get_type_info(serializer.type_).type_id
- if is_union_type(type_id):
- # customized types can't be detected at compile time for some languages
- type_id = TypeId.UNKNOWN
- is_nullable = nullable_map.get(field_name, False)
-
- # For polymorphic or enum types, set type_id to UNKNOWN but preserve nullable from map
- if is_polymorphic_type(type_id) or type_id in {
- TypeId.ENUM,
- TypeId.NAMED_ENUM,
- }:
- type_id = TypeId.UNKNOWN
-
- # Use nullable from map - for xlang, this is already computed correctly
- # (False by default except for Optional[T] or explicit annotation)
- nullable_flag = "1" if is_nullable else "0"
+ nullable = fi.nullable if fi else nullable_map.get(field_name, False)
+ ref = fi.ref if fi else False
+ type_hint = fi.type_hint if fi else (serializer.type_ if serializer is not None else typing.Any)
+ type_fingerprint = _build_schema_fingerprint_type(
+ type_resolver,
+ type_hint,
+ nullable=nullable,
+ track_ref=ref,
+ include_ref=True,
+ )
# Determine field identifier for fingerprint
if tag_id >= 0:
@@ -892,19 +882,132 @@ def compute_struct_fingerprint(type_resolver, field_names, serializers, nullable
# Sort by field name (lexicographic) for name-based fields
sort_key = (1, field_name, "") # 1 = name fields come after
- fp_fields.append((sort_key, field_id_or_name, type_id, ref_flag, nullable_flag))
+ fp_fields.append((sort_key, field_id_or_name, type_fingerprint))
# Sort fields: tag ID fields first (by ID), then name fields (lexicographically)
fp_fields.sort(key=lambda x: x[0])
# Build fingerprint string
hash_parts = []
- for _, field_id_or_name, type_id, ref_flag, nullable_flag in fp_fields:
- hash_parts.append(f"{field_id_or_name},{type_id},{ref_flag},{nullable_flag};")
+ for _, field_id_or_name, type_fingerprint in fp_fields:
+ hash_parts.append(f"{field_id_or_name},{type_fingerprint};")
return "".join(hash_parts)
+def _normalize_schema_fingerprint_type_id(type_id):
+ if type_id in {
+ TypeId.UNKNOWN,
+ TypeId.ENUM,
+ TypeId.NAMED_ENUM,
+ TypeId.STRUCT,
+ TypeId.COMPATIBLE_STRUCT,
+ TypeId.NAMED_STRUCT,
+ TypeId.NAMED_COMPATIBLE_STRUCT,
+ TypeId.EXT,
+ TypeId.NAMED_EXT,
+ TypeId.UNION,
+ TypeId.TYPED_UNION,
+ TypeId.NAMED_UNION,
+ }:
+ return TypeId.UNKNOWN
+ return type_id
+
+
+def _leaf_schema_fingerprint_type_id(type_resolver, type_hint):
+ if type_hint is typing.Any or type_hint is object:
+ return TypeId.UNKNOWN
+ if is_primitive_array_type(type_hint):
+ return type_resolver.get_type_info(type_hint).type_id
+ if is_subclass(type_hint, enum.Enum):
+ return _normalize_schema_fingerprint_type_id(type_resolver.get_type_info(type_hint).type_id)
+ if type_hint in basic_types:
+ return _normalize_schema_fingerprint_type_id(type_resolver.get_type_info(type_hint).type_id)
+ typeinfo = type_resolver.get_type_info(type_hint, create=False)
+ if typeinfo is None:
+ return TypeId.UNKNOWN
+ return _normalize_schema_fingerprint_type_id(typeinfo.type_id)
+
+
+def _build_schema_fingerprint_type(type_resolver, type_hint, nullable, track_ref, include_ref, include_nullable=True):
+ type_hint, ref_override = unwrap_ref(type_hint)
+ if include_ref and ref_override is not None:
+ track_ref = ref_override
+ if not include_ref:
+ track_ref = False
+
+ unwrapped_type, is_optional = unwrap_optional(type_hint)
+ nullable = nullable or is_optional
+ origin = _get_origin(unwrapped_type) or getattr(unwrapped_type, "__origin__", unwrapped_type)
+ args = _get_args(unwrapped_type)
+
+ ref_flag = "1" if track_ref else "0"
+ nullable_flag = "1" if include_nullable and nullable else "0"
+
+ if args:
+ if origin is list or origin == typing.List:
+ elem_type = args[0]
+ child = _build_schema_fingerprint_type(
+ type_resolver,
+ elem_type,
+ nullable=True,
+ track_ref=False,
+ include_ref=False,
+ include_nullable=False,
+ )
+ return f"{TypeId.LIST},{ref_flag},{nullable_flag}[{child}]"
+ if origin is set or origin == typing.Set:
+ elem_type = args[0]
+ child = _build_schema_fingerprint_type(
+ type_resolver,
+ elem_type,
+ nullable=True,
+ track_ref=False,
+ include_ref=False,
+ include_nullable=False,
+ )
+ return f"{TypeId.SET},{ref_flag},{nullable_flag}[{child}]"
+ if origin is tuple or origin == typing.Tuple:
+ elem_type = get_homogeneous_tuple_elem_type(args)
+ if elem_type is None:
+ child = f"{TypeId.UNKNOWN},0,0"
+ else:
+ child = _build_schema_fingerprint_type(
+ type_resolver,
+ elem_type,
+ nullable=True,
+ track_ref=False,
+ include_ref=False,
+ include_nullable=False,
+ )
+ return f"{TypeId.LIST},{ref_flag},{nullable_flag}[{child}]"
+ if origin is dict or origin == typing.Dict:
+ key_type, value_type = args
+ key = _build_schema_fingerprint_type(
+ type_resolver,
+ key_type,
+ nullable=True,
+ track_ref=False,
+ include_ref=False,
+ include_nullable=False,
+ )
+ value = _build_schema_fingerprint_type(
+ type_resolver,
+ value_type,
+ nullable=True,
+ track_ref=False,
+ include_ref=False,
+ include_nullable=False,
+ )
+ return f"{TypeId.MAP},{ref_flag},{nullable_flag}[{key}|{value}]"
+ if origin is typing.Union:
+ type_id = TypeId.UNKNOWN
+ return f"{type_id},{ref_flag},{nullable_flag}"
+
+ type_id = _leaf_schema_fingerprint_type_id(type_resolver, unwrapped_type)
+ return f"{type_id},{ref_flag},{nullable_flag}"
+
+
def compute_struct_meta(type_resolver, field_names, serializers, nullable_map=None, field_infos_list=None):
"""
Computes struct metadata including version hash, sorted field names, and serializers.
diff --git a/python/pyfory/tests/test_cross_language.py b/python/pyfory/tests/test_cross_language.py
index e608b80eb..e70fad630 100644
--- a/python/pyfory/tests/test_cross_language.py
+++ b/python/pyfory/tests/test_cross_language.py
@@ -519,6 +519,7 @@ def test_struct_hash(data_file_path):
serializer._field_names,
serializer._serializers,
serializer._nullable_fields,
+ serializer._field_infos,
)[0]
assert read_hash == v, (read_hash, v)
diff --git a/rust/fory-derive/src/object/util.rs b/rust/fory-derive/src/object/util.rs
index 7cdabb2e4..1539c7602 100644
--- a/rust/fory-derive/src/object/util.rs
+++ b/rust/fory-derive/src/object/util.rs
@@ -585,16 +585,10 @@ pub(super) fn get_sort_fields_ts(fields: &[&Field]) -> TokenStream {
/// Field metadata for fingerprint computation.
struct FieldFingerprintInfo {
- /// Field name (snake_case) or field ID as string
+ /// Field name (snake_case) or field ID as string.
name_or_id: String,
- /// Whether the field has explicit nullable=true/false set via #[fory(nullable)]
- explicit_nullable: Option<bool>,
- /// Whether reference tracking is enabled
- track_ref: bool,
- /// The type ID (UNKNOWN for user-defined types including enums/unions)
- type_id: u32,
- /// Whether the field type is `Option<T>`
- is_option_type: bool,
+ /// Recursive field type fingerprint.
+ type_fingerprint: String,
}
/// Adjusts integer type IDs based on semantic `encoding` attributes.
@@ -616,12 +610,152 @@ fn adjust_type_id_for_encoding(base_type_id: u32, meta: &super::field_meta::Fory
}
}
+fn fingerprint_type_id(type_id: u32) -> u32 {
+ if type_id == TypeId::UNKNOWN as u32
+ || type_id == TypeId::UNION as u32
+ || type_id == TypeId::TYPED_UNION as u32
+ || type_id == TypeId::NAMED_UNION as u32
+ {
+ TypeId::UNKNOWN as u32
+ } else {
+ type_id
+ }
+}
+
+fn type_name_and_args(
+ ty: &Type,
+) -> Option<(
+ String,
+ Option<&syn::punctuated::Punctuated<GenericArgument, syn::token::Comma>>,
+)> {
+ let Type::Path(type_path) = ty else {
+ return None;
+ };
+ let seg = type_path.path.segments.last()?;
+ let PathArguments::AngleBracketed(args) = &seg.arguments else {
+ return Some((seg.ident.to_string(), None));
+ };
+ Some((seg.ident.to_string(), Some(&args.args)))
+}
+
+fn single_type_arg(
+ args: &syn::punctuated::Punctuated<GenericArgument, syn::token::Comma>,
+) -> Option<&Type> {
+ args.iter().find_map(|arg| match arg {
+ GenericArgument::Type(ty) => Some(ty),
+ _ => None,
+ })
+}
+
+fn two_type_args(
+ args: &syn::punctuated::Punctuated<GenericArgument, syn::token::Comma>,
+) -> Option<(&Type, &Type)> {
+ let mut iter = args.iter().filter_map(|arg| match arg {
+ GenericArgument::Type(ty) => Some(ty),
+ _ => None,
+ });
+ Some((iter.next()?, iter.next()?))
+}
+
+fn build_type_fingerprint(
+ ty: &Type,
+ meta: &super::field_meta::ForyFieldMeta,
+ include_ref: bool,
+ include_nullable: bool,
+) -> String {
+ use super::field_meta::{classify_field_type, extract_option_inner_type, is_option_type};
+
+ let type_class = classify_field_type(ty);
+ let nullable = meta.effective_nullable(type_class) || is_option_type(ty);
+ let track_ref = include_ref && meta.effective_ref(type_class);
+ let type_id = fingerprint_type_id(adjust_type_id_for_encoding(
+ get_type_id_by_type_ast(ty),
+ meta,
+ ));
+
+ let mut fingerprint = format!(
+ "{},{},{}",
+ type_id,
+ if track_ref { 1 } else { 0 },
+ if include_nullable && nullable { 1 } else { 0 }
+ );
+
+ let container_ty = extract_option_inner_type(ty).unwrap_or_else(|| ty.clone());
+
+ if let Type::Array(array) = &container_ty {
+ if type_id == TypeId::LIST as u32 {
+ fingerprint.push('[');
+ fingerprint.push_str(&build_type_fingerprint(
+ array.elem.as_ref(),
+ &meta.element_meta(),
+ false,
+ false,
+ ));
+ fingerprint.push(']');
+ }
+ return fingerprint;
+ }
+
+ let Some((name, args)) = type_name_and_args(&container_ty) else {
+ return fingerprint;
+ };
+
+ match name.as_str() {
+ "Vec" | "VecDeque" | "LinkedList" if type_id == TypeId::LIST as u32 => {
+ if let Some(elem_ty) = args.and_then(single_type_arg) {
+ fingerprint.push('[');
+ fingerprint.push_str(&build_type_fingerprint(
+ elem_ty,
+ &meta.element_meta(),
+ false,
+ false,
+ ));
+ fingerprint.push(']');
+ }
+ }
+ "HashSet" | "BTreeSet" | "BinaryHeap" if type_id == TypeId::SET as u32 => {
+ if let Some(elem_ty) = args.and_then(single_type_arg) {
+ fingerprint.push('[');
+ fingerprint.push_str(&build_type_fingerprint(
+ elem_ty,
+ &meta.element_meta(),
+ false,
+ false,
+ ));
+ fingerprint.push(']');
+ }
+ }
+ "HashMap" | "BTreeMap" if type_id == TypeId::MAP as u32 => {
+ if let Some((key_ty, value_ty)) = args.and_then(two_type_args) {
+ fingerprint.push('[');
+ fingerprint.push_str(&build_type_fingerprint(
+ key_ty,
+ &meta.map_key_meta(),
+ false,
+ false,
+ ));
+ fingerprint.push('|');
+ fingerprint.push_str(&build_type_fingerprint(
+ value_ty,
+ &meta.map_value_meta(),
+ false,
+ false,
+ ));
+ fingerprint.push(']');
+ }
+ }
+ _ => {}
+ }
+
+ fingerprint
+}
+
/// Computes struct fingerprint string at compile time (during proc-macro execution).
///
-/// **Fingerprint Format:** `<field_name_or_id>,<type_id>,<ref>,<nullable>;`
+/// **Fingerprint Format:** `<field_name_or_id>,<type_id>,<ref>,<nullable>[<child...>];`
/// Fields are sorted by name lexicographically.
fn compute_struct_fingerprint(fields: &[&Field]) -> String {
- use super::field_meta::{classify_field_type, parse_field_meta};
+ use super::field_meta::parse_field_meta;
let mut field_infos: Vec<FieldFingerprintInfo> = Vec::with_capacity(fields.len());
@@ -639,30 +773,9 @@ fn compute_struct_fingerprint(fields: &[&Field]) -> String {
to_snake_case(&name)
};
- let type_class = classify_field_type(&field.ty);
- let track_ref = meta.effective_ref(type_class);
- let explicit_nullable = meta.nullable;
-
- // Get compile-time TypeId, considering encoding attributes for u32/u64 fields
- let base_type_id = get_type_id_by_type_ast(&field.ty);
- let type_id = adjust_type_id_for_encoding(base_type_id, &meta);
-
- // Check if field type is Option<T>
- let ty_str: String = field
- .ty
- .to_token_stream()
- .to_string()
- .chars()
- .filter(|c| !c.is_whitespace())
- .collect();
- let is_option_type = ty_str.starts_with("Option<");
-
field_infos.push(FieldFingerprintInfo {
name_or_id,
- explicit_nullable,
- track_ref,
- type_id,
- is_option_type,
+ type_fingerprint: build_type_fingerprint(&field.ty, &meta, true, true),
});
}
@@ -672,32 +785,9 @@ fn compute_struct_fingerprint(fields: &[&Field]) -> String {
// Build fingerprint string
let mut fingerprint = String::new();
for info in &field_infos {
- let ref_flag = if info.track_ref { "1" } else { "0" };
- let nullable = match info.explicit_nullable {
- Some(true) => true,
- Some(false) => false,
- None => info.is_option_type,
- };
- let nullable_flag = if nullable { "1" } else { "0" };
-
- // User-defined types (UNKNOWN) and unions use 0 in fingerprint, matching Java behavior
- let effective_type_id = if info.type_id == TypeId::UNKNOWN as u32
- || info.type_id == TypeId::UNION as u32
- || info.type_id == TypeId::TYPED_UNION as u32
- || info.type_id == TypeId::NAMED_UNION as u32
- {
- 0
- } else {
- info.type_id
- };
-
fingerprint.push_str(&info.name_or_id);
fingerprint.push(',');
- fingerprint.push_str(&effective_type_id.to_string());
- fingerprint.push(',');
- fingerprint.push_str(ref_flag);
- fingerprint.push(',');
- fingerprint.push_str(nullable_flag);
+ fingerprint.push_str(&info.type_fingerprint);
fingerprint.push(';');
}
diff --git a/swift/Sources/Fory/MacroDeclarations.swift b/swift/Sources/Fory/MacroDeclarations.swift
index 60995726e..e1c34b9bd 100644
--- a/swift/Sources/Fory/MacroDeclarations.swift
+++ b/swift/Sources/Fory/MacroDeclarations.swift
@@ -20,6 +20,7 @@
names: named(staticTypeId),
named(foryEvolving),
named(isRefType),
+ named(__foryNormalizeSchemaFingerprintTypeID),
named(__forySchemaHash),
named(__forySchemaHashTrackRefDisabled),
named(__forySchemaHashTrackRefEnabled),
@@ -43,6 +44,7 @@ public macro ForyStruct(evolving: Bool = true) = #externalMacro(module: "ForyMac
names: named(staticTypeId),
named(foryEvolving),
named(isRefType),
+ named(__foryNormalizeSchemaFingerprintTypeID),
named(__forySchemaHash),
named(__forySchemaHashTrackRefDisabled),
named(__forySchemaHashTrackRefEnabled),
@@ -66,6 +68,7 @@ public macro ForyEnum() = #externalMacro(module: "ForyMacro", type: "ForyEnumMac
names: named(staticTypeId),
named(foryEvolving),
named(isRefType),
+ named(__foryNormalizeSchemaFingerprintTypeID),
named(__forySchemaHash),
named(__forySchemaHashTrackRefDisabled),
named(__forySchemaHashTrackRefEnabled),
diff --git a/swift/Sources/ForyMacro/ForyObjectMacro.swift b/swift/Sources/ForyMacro/ForyObjectMacro.swift
index b07ae61a8..1e68ae3b8 100644
--- a/swift/Sources/ForyMacro/ForyObjectMacro.swift
+++ b/swift/Sources/ForyMacro/ForyObjectMacro.swift
@@ -64,7 +64,7 @@ public struct ForyStructMacro: MemberMacro, ExtensionMacro {
\(raw: accessPrefix)static var isRefType: Bool { true }
""" : nil
- let schemaHashDecl: DeclSyntax = DeclSyntax(stringLiteral: buildSchemaHashDecl(fields: parsed.fields))
+ let schemaHashDecl: DeclSyntax = DeclSyntax(stringLiteral: try buildSchemaHashDecl(fields: parsed.fields))
let compatibleTypeMetaDecl: DeclSyntax = DeclSyntax(
stringLiteral: buildCompatibleTypeMetaFieldsDecl(sortedFields: sortedFields, accessPrefix: accessPrefix)
)
@@ -273,6 +273,7 @@ enum DynamicAnyCodecKind {
struct ParsedField {
let name: String
let typeText: String
+ fileprivate let typeHint: FieldTypeHint?
let originalIndex: Int
let isOptional: Bool
@@ -735,6 +736,7 @@ private func parseFields(_ declaration: some DeclGroupSyntax) throws -> ParsedDe
ParsedField(
name: name,
typeText: rawType,
+ typeHint: fieldTypeHint,
originalIndex: originalIndex,
isOptional: isOptional,
isCollection: classification.isCollection || classification.isMap,
@@ -1912,15 +1914,24 @@ private func sortFields(_ fields: [ParsedField]) -> [ParsedField] {
}
}
-private func buildSchemaHashDecl(fields: [ParsedField]) -> String {
- let fingerprintTrackRefDisabled = buildSchemaFingerprint(fields: fields, trackRefExpression: "false")
- let fingerprintTrackRefEnabled = buildSchemaFingerprint(fields: fields, trackRefExpression: "true")
+private func buildSchemaHashDecl(fields: [ParsedField]) throws -> String {
+ let fingerprintTrackRefDisabled = try buildSchemaFingerprint(fields: fields, trackRefExpression: "false")
+ let fingerprintTrackRefEnabled = try buildSchemaFingerprint(fields: fields, trackRefExpression: "true")
return """
+ private static func __foryNormalizeSchemaFingerprintTypeID(_ typeID: UInt32) -> UInt32 {
+ switch typeID {
+ case 0, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35:
+ return 0
+ default:
+ return typeID
+ }
+ }
+
private static let __forySchemaHashTrackRefDisabled: UInt32 = SchemaHash.structHash32(\(fingerprintTrackRefDisabled))
private static let __forySchemaHashTrackRefEnabled: UInt32 = SchemaHash.structHash32(\(fingerprintTrackRefEnabled))
private static func __forySchemaHash(_ trackRef: Bool) -> UInt32 {
- trackRef ? Self.__forySchemaHashTrackRefEnabled : Self.__forySchemaHashTrackRefDisabled
+ trackRef ? __forySchemaHashTrackRefEnabled : __forySchemaHashTrackRefDisabled
}
"""
}
@@ -1933,7 +1944,7 @@ private func buildCompatibleTypeMetaFieldsDecl(sortedFields: [ParsedField], acce
private static let __foryFieldsInfoTrackRefEnabled: [TypeMeta.FieldInfo] = \(enabledExpr)
\(accessPrefix)static func foryFieldsInfo(trackRef: Bool) -> [TypeMeta.FieldInfo] {
- trackRef ? Self.__foryFieldsInfoTrackRefEnabled : Self.__foryFieldsInfoTrackRefDisabled
+ trackRef ? __foryFieldsInfoTrackRefEnabled : __foryFieldsInfoTrackRefDisabled
}
"""
}
@@ -1959,48 +1970,193 @@ private func compatibleFieldIDExpr(_ field: ParsedField) -> String {
return "nil"
}
-private func buildSchemaFingerprint(fields: [ParsedField], trackRefExpression: String) -> String {
- let entries = fields
+private func buildSchemaFingerprint(fields: [ParsedField], trackRefExpression: String) throws -> String {
+ let sortedFields = fields
.sorted { lhs, rhs in
if lhs.schemaIdentifier != rhs.schemaIdentifier {
return lhs.schemaIdentifier < rhs.schemaIdentifier
}
return lhs.originalIndex < rhs.originalIndex
}
- .map { field -> String in
- let typeID = fingerprintTypeID(for: field)
- let nullable = field.isOptional ? "1" : "0"
- let trackRefExpr: String
- if let dynamicAnyCodec = field.dynamicAnyCodec {
- switch dynamicAnyCodec {
- case .anyValue:
- trackRefExpr = "(\(trackRefExpression)) ? 1 : 0"
- case .anyHashableValue, .anyList, .stringAnyMap, .int32AnyMap, .anyHashableAnyMap:
- trackRefExpr = "0"
- }
- } else if let customCodecType = field.customCodecType {
- trackRefExpr = "((\(trackRefExpression)) && \(customCodecType).isRefType) ? 1 : 0"
- } else {
- trackRefExpr = "((\(trackRefExpression)) && \(field.typeText).isRefType) ? 1 : 0"
- }
- return "\"\(field.schemaIdentifier),\(typeID),\\(\(trackRefExpr)),\(nullable);\""
- }
+ let entries = try sortedFields.map { field -> String in
+ let typeFingerprint = try buildSchemaFieldTypeFingerprint(field, trackRefExpression: trackRefExpression)
+ return "\"\(field.schemaIdentifier),\" + \(typeFingerprint) + \";\""
+ }
if entries.isEmpty {
return "\"\""
}
return entries.joined(separator: " + ")
}
-private func fingerprintTypeID(for field: ParsedField) -> UInt32 {
- if field.customCodecType != nil {
- return field.typeID
+private func buildSchemaFieldTypeFingerprint(
+ _ field: ParsedField,
+ trackRefExpression: String
+) throws -> String {
+ let fieldTrackRefExpression: String
+ if let dynamicAnyCodec = field.dynamicAnyCodec {
+ fieldTrackRefExpression = dynamicAnyUsesContextTrackRef(dynamicAnyCodec) ? trackRefExpression : "false"
+ } else if let customCodecType = field.customCodecType {
+ fieldTrackRefExpression = "\(trackRefExpression) && \(customCodecType).isRefType"
+ } else {
+ fieldTrackRefExpression = "\(trackRefExpression) && \(field.typeText).isRefType"
}
- let optional = unwrapOptional(field.typeText)
- let classification = classifyType(optional.type)
- if classification.isPrimitive || classification.isBuiltIn {
- return field.typeID
+
+ return try buildSchemaTypeFingerprint(
+ typeText: field.typeText,
+ hint: field.typeHint,
+ nullableExpression: field.isOptional ? "true" : "false",
+ trackRefExpression: fieldTrackRefExpression,
+ explicitTypeIDExpression: field.customCodecType != nil ? "\(field.typeID)" : nil
+ )
+}
+
+private func buildSchemaTypeFingerprint(
+ typeText: String,
+ hint: FieldTypeHint?,
+ nullableExpression: String,
+ trackRefExpression: String,
+ includeNullable: Bool = true,
+ explicitTypeIDExpression: String? = nil
+) throws -> String {
+ let normalized = trimType(typeText)
+ let optional = unwrapOptional(normalized)
+ let concreteType = optional.type
+ let outerClassification = classifyType(concreteType)
+ let nullableLiteral = includeNullable && nullableExpression == "true" ? "1" : "0"
+ let trackFlagExpr = trackRefExpression == "false" ? "0" : "((\(trackRefExpression)) ? 1 : 0)"
+
+ if let hint {
+ switch hint {
+ case .list(let elementHint):
+ let listClassification = try classification(for: concreteType, hint: hint)
+ if listClassification.typeID != 22 {
+ let typeIDExpr = explicitTypeIDExpression ?? "\(listClassification.typeID)"
+ return "\"\\(__foryNormalizeSchemaFingerprintTypeID(\(typeIDExpr))),\\(\(trackFlagExpr)),\(nullableLiteral)\""
+ }
+ let elementType = parseArrayElement(concreteType) ?? hintedValueTypeName(elementHint)
+ guard let elementType else {
+ throw MacroExpansionErrorMessage(
+ "list field hint requires an Array/List Swift type or a full element type hint"
+ )
+ }
+ let elementNullable = compatibleGenericNullableExpression(elementType)
+ let elementExpr = try buildSchemaTypeFingerprint(
+ typeText: elementType,
+ hint: elementHint,
+ nullableExpression: elementNullable,
+ trackRefExpression: "false",
+ includeNullable: false
+ )
+ let prefix = "\"\\(__foryNormalizeSchemaFingerprintTypeID(UInt32(TypeId.list.rawValue))),\\(\(trackFlagExpr)),\(nullableLiteral)\""
+ return "\(prefix) + \"[\" + \(elementExpr) + \"]\""
+ case .set(let elementHint):
+ let elementType = parseSetElement(concreteType) ?? hintedValueTypeName(elementHint)
+ guard let elementType else {
+ throw MacroExpansionErrorMessage(
+ "set field hint requires a Set Swift type or a full element type hint"
+ )
+ }
+ let elementNullable = compatibleGenericNullableExpression(elementType)
+ let elementExpr = try buildSchemaTypeFingerprint(
+ typeText: elementType,
+ hint: elementHint,
+ nullableExpression: elementNullable,
+ trackRefExpression: "false",
+ includeNullable: false
+ )
+ let prefix = "\"\\(__foryNormalizeSchemaFingerprintTypeID(UInt32(TypeId.set.rawValue))),\\(\(trackFlagExpr)),\(nullableLiteral)\""
+ return "\(prefix) + \"[\" + \(elementExpr) + \"]\""
+ case .map(let keyHint, let valueHint):
+ let parsedMap = parseDictionary(concreteType)
+ let keyType = parsedMap?.0 ?? keyHint.flatMap(hintedValueTypeName)
+ let valueType = parsedMap?.1 ?? valueHint.flatMap(hintedValueTypeName)
+ guard let keyType, let valueType else {
+ throw MacroExpansionErrorMessage(
+ "map field hint requires a Dictionary/Map Swift type or full key/value hints"
+ )
+ }
+ let keyNullable = compatibleGenericNullableExpression(keyType)
+ let valueNullable = compatibleGenericNullableExpression(valueType)
+ let keyExpr = try buildSchemaTypeFingerprint(
+ typeText: keyType,
+ hint: keyHint,
+ nullableExpression: keyNullable,
+ trackRefExpression: "false",
+ includeNullable: false
+ )
+ let valueExpr = try buildSchemaTypeFingerprint(
+ typeText: valueType,
+ hint: valueHint,
+ nullableExpression: valueNullable,
+ trackRefExpression: "false",
+ includeNullable: false
+ )
+ let prefix = "\"\\(__foryNormalizeSchemaFingerprintTypeID(UInt32(TypeId.map.rawValue))),\\(\(trackFlagExpr)),\(nullableLiteral)\""
+ return "\(prefix) + \"[\" + \(keyExpr) + \"|\" + \(valueExpr) + \"]\""
+ case .inferredEncoding, .scalar:
+ break
+ }
+ }
+
+ if outerClassification.typeID == 22, let elementType = parseArrayElement(concreteType) {
+ let elementNullable = compatibleGenericNullableExpression(elementType)
+ let elementExpr = try buildSchemaTypeFingerprint(
+ typeText: elementType,
+ hint: nil,
+ nullableExpression: elementNullable,
+ trackRefExpression: "false",
+ includeNullable: false
+ )
+ let prefix = "\"\\(__foryNormalizeSchemaFingerprintTypeID(UInt32(TypeId.list.rawValue))),\\(\(trackFlagExpr)),\(nullableLiteral)\""
+ return "\(prefix) + \"[\" + \(elementExpr) + \"]\""
+ }
+
+ if outerClassification.typeID == 23, let elementType = parseSetElement(concreteType) {
+ let elementNullable = compatibleGenericNullableExpression(elementType)
+ let elementExpr = try buildSchemaTypeFingerprint(
+ typeText: elementType,
+ hint: nil,
+ nullableExpression: elementNullable,
+ trackRefExpression: "false",
+ includeNullable: false
+ )
+ let prefix = "\"\\(__foryNormalizeSchemaFingerprintTypeID(UInt32(TypeId.set.rawValue))),\\(\(trackFlagExpr)),\(nullableLiteral)\""
+ return "\(prefix) + \"[\" + \(elementExpr) + \"]\""
}
- return 0
+
+ if outerClassification.typeID == 24, let (keyType, valueType) = parseDictionary(concreteType) {
+ let keyNullable = compatibleGenericNullableExpression(keyType)
+ let valueNullable = compatibleGenericNullableExpression(valueType)
+ let keyExpr = try buildSchemaTypeFingerprint(
+ typeText: keyType,
+ hint: nil,
+ nullableExpression: keyNullable,
+ trackRefExpression: "false",
+ includeNullable: false
+ )
+ let valueExpr = try buildSchemaTypeFingerprint(
+ typeText: valueType,
+ hint: nil,
+ nullableExpression: valueNullable,
+ trackRefExpression: "false",
+ includeNullable: false
+ )
+ let prefix = "\"\\(__foryNormalizeSchemaFingerprintTypeID(UInt32(TypeId.map.rawValue))),\\(\(trackFlagExpr)),\(nullableLiteral)\""
+ return "\(prefix) + \"[\" + \(keyExpr) + \"|\" + \(valueExpr) + \"]\""
+ }
+
+ let typeIDExpr: String
+ if let explicitTypeIDExpression {
+ typeIDExpr = explicitTypeIDExpression
+ } else if let hint {
+ typeIDExpr = "\(try classification(for: concreteType, hint: hint).typeID)"
+ } else if isDynamicAnyConcreteType(concreteType) {
+ typeIDExpr = "UInt32(TypeId.unknown.rawValue)"
+ } else {
+ typeIDExpr = compatibleFieldTypeIDExpression(concreteType)
+ }
+
+ return "\"\\(__foryNormalizeSchemaFingerprintTypeID(\(typeIDExpr))),\\(\(trackFlagExpr)),\(nullableLiteral)\""
}
private func buildDefaultDecl(isClass: Bool, fields: [ParsedField], accessPrefix: String) -> String {
diff --git a/swift/Tests/ForyTests/CompatibilityTests.swift b/swift/Tests/ForyTests/CompatibilityTests.swift
index 91a11d98e..6633e0bf7 100644
--- a/swift/Tests/ForyTests/CompatibilityTests.swift
+++ b/swift/Tests/ForyTests/CompatibilityTests.swift
@@ -101,6 +101,12 @@ private struct RemoteNestedFixedMapV1: Equatable {
var ids: Set<Int32?> = []
}
+@ForyStruct
+private struct SchemaHashNestedAnnotatedContainer: Equatable {
+ @MapField(key: .uint32(encoding: .fixed), value: .list(element: .uint64(encoding: .tagged)))
+ var values: [UInt32?: [UInt64?]?] = [:]
+}
+
@ForyStruct
private struct LocalNestedVarintMapV2: Equatable {
@ForyField(id: 1)
@@ -243,6 +249,31 @@ func schemaHashMatchesJavaFingerprintForTaggedUnsignedFields() {
)
}
+@Test
+func schemaHashUsesNestedAnnotatedContainerTypeIDs() throws {
+ let fory = Fory(config: .init(xlang: true, trackRef: false, compatible: false, checkClassVersion: true))
+ fory.register(SchemaHashNestedAnnotatedContainer.self, id: 9912)
+
+ let bytes = try fory.serialize(
+ SchemaHashNestedAnnotatedContainer(
+ values: [
+ UInt32(7): [UInt64(11), nil]
+ ]
+ )
+ )
+ let buffer = ByteBuffer(data: bytes)
+ _ = try fory.readHead(buffer: buffer)
+ _ = try buffer.readInt8()
+ _ = try buffer.readVarUInt32()
+ _ = try buffer.readVarUInt32()
+ let schemaHash = UInt32(bitPattern: try buffer.readInt32())
+
+ #expect(
+ schemaHash ==
+ SchemaHash.structHash32("values,24,0,0[11,0,0|22,0,0[15,0,0]];")
+ )
+}
+
@Test
func compatibleNestedArrayEvolves() throws {
let writerV1 = Fory(config: .init(xlang: true, trackRef: false, compatible: true))
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@fory.apache.org
For additional commands, e-mail: commits-help@fory.apache.org