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 477105cb7 feat(rust): forbid late type registration after resolver
snapshot ini… (#3435)
477105cb7 is described below
commit 477105cb73785093222d27d13c3ae6cbde4eeb49
Author: Geethapranay1 <[email protected]>
AuthorDate: Thu Mar 5 18:32:54 2026 +0530
feat(rust): forbid late type registration after resolver snapshot ini…
(#3435)
Add a lifecycle guard to all registration entry points in fory. After
the first serialize/deserialize call initializes the
`final_type_resolver` (`OnceLock`), any subsequent `register*` call
returns `Error::NotAllowed` with a descriptive message.
this prevents a silent footgun where late registrations appear to
succeed but are never reflected in the frozen resolver snapshot.
## Guarded Methods
- `register`, `register_union`
- `register_by_name`, `register_union_by_name`
- `register_by_namespace`, `register_union_by_namespace`
- `register_serializer`, `register_serializer_by_name`,
`register_serializer_by_namespace`
- `register_generic_trait`
## Why?
After the first serialize/deserialize call, Fory's `final_type_resolver`
(`OnceLock`) is frozen. Any subsequent type registration calls
(`register*`) silently succeed but do not affect the frozen resolver,
leading to confusing runtime errors.
this PR introduces a **lifecycle guard** to prevent late registration
and make the lifecycle explicit and fail-fast.
## What does this PR do?
* adds a lifecycle guard (`check_registration_allowed`) to all type
registration entry points in fory.
* after the first serialize/deserialize call, any further registration
attempts return `Error::NotAllowed` with a descriptive message.
* prevents silent bugs where late registrations appear to succeed but
are ignored at runtime.
* adds comprehensive tests to verify both positive and negative cases
for all registration methods.
## Guarded Methods
* `register`, `register_union`
* `register_by_name`, `register_union_by_name`
* `register_by_namespace`, `register_union_by_namespace`
* `register_serializer`, `register_serializer_by_name`,
`register_serializer_by_namespace`
* `register_generic_trait`
## Related issues
Closes #3417
## Does this PR introduce any user-facing change?
Yes. users will now receive a clear error if they attempt to register
types after the resolver snapshot is initialized, instead of silent
failure.
- [ ] Does this PR introduce any public API change?
- [ ] Does this PR introduce any binary protocol compatibility change?
## Benchmark
after updating code changes cargo build successful
<img width="974" height="112" alt="Screenshot From 2026-02-28 14-59-24"
src="https://github.com/user-attachments/assets/db3b9bb3-4f04-4339-aeed-4ad0351522ac"
/>
testing with new test file successfully passed all testcases
<img width="978" height="525" alt="Screenshot From 2026-02-28 15-00-21"
src="https://github.com/user-attachments/assets/e9f20643-f84d-4f1a-a694-c80567fde0b8"
/>
---
rust/fory-core/src/fory.rs | 31 ++++
rust/tests/tests/test_lifecycle_guard.rs | 273 +++++++++++++++++++++++++++++++
2 files changed, 304 insertions(+)
diff --git a/rust/fory-core/src/fory.rs b/rust/fory-core/src/fory.rs
index 9d3f82694..d4e2359b3 100644
--- a/rust/fory-core/src/fory.rs
+++ b/rust/fory-core/src/fory.rs
@@ -372,6 +372,27 @@ impl Fory {
&self.config
}
+ /// Checks whether the final type resolver has already been initialized.
+ ///
+ /// If it has, further type registrations would be silently ignored (the
frozen
+ /// snapshot is what serialize/deserialize actually use),so we fail fast
with
+ /// a clear error instead.
+ ///
+ /// # errors
+ ///
+ /// returns [`Error::NotAllowed`] when the resolver snapshot has already
been
+ /// built (i.e after the first `serialize` / `deserialize` call).
+ fn check_registration_allowed(&self) -> Result<(), Error> {
+ if self.final_type_resolver.get().is_some() {
+ return Err(Error::not_allowed(
+ "Type registration is not allowed after the first
serialize/deserialize call. \
+ The type resolver snapshot has already been finalized. \
+ Please complete all type registrations before performing any
serialization or deserialization.",
+ ));
+ }
+ Ok(())
+ }
+
/// Serializes a value of type `T` into a byte vector.
///
/// # Type Parameters
@@ -655,6 +676,7 @@ impl Fory {
&mut self,
id: u32,
) -> Result<(), Error> {
+ self.check_registration_allowed()?;
self.type_resolver.register_by_id::<T>(id)
}
@@ -665,6 +687,7 @@ impl Fory {
&mut self,
id: u32,
) -> Result<(), Error> {
+ self.check_registration_allowed()?;
self.type_resolver.register_union_by_id::<T>(id)
}
@@ -703,6 +726,7 @@ impl Fory {
namespace: &str,
type_name: &str,
) -> Result<(), Error> {
+ self.check_registration_allowed()?;
self.type_resolver
.register_by_namespace::<T>(namespace, type_name)
}
@@ -715,6 +739,7 @@ impl Fory {
namespace: &str,
type_name: &str,
) -> Result<(), Error> {
+ self.check_registration_allowed()?;
self.type_resolver
.register_union_by_namespace::<T>(namespace, type_name)
}
@@ -749,6 +774,7 @@ impl Fory {
&mut self,
type_name: &str,
) -> Result<(), Error> {
+ self.check_registration_allowed()?;
self.register_by_namespace::<T>("", type_name)
}
@@ -757,6 +783,7 @@ impl Fory {
&mut self,
type_name: &str,
) -> Result<(), Error> {
+ self.check_registration_allowed()?;
self.register_union_by_namespace::<T>("", type_name)
}
@@ -791,6 +818,7 @@ impl Fory {
&mut self,
id: u32,
) -> Result<(), Error> {
+ self.check_registration_allowed()?;
self.type_resolver.register_serializer_by_id::<T>(id)
}
@@ -815,6 +843,7 @@ impl Fory {
namespace: &str,
type_name: &str,
) -> Result<(), Error> {
+ self.check_registration_allowed()?;
self.type_resolver
.register_serializer_by_namespace::<T>(namespace, type_name)
}
@@ -836,6 +865,7 @@ impl Fory {
&mut self,
type_name: &str,
) -> Result<(), Error> {
+ self.check_registration_allowed()?;
self.register_serializer_by_namespace::<T>("", type_name)
}
@@ -845,6 +875,7 @@ impl Fory {
pub fn register_generic_trait<T: 'static + Serializer + ForyDefault>(
&mut self,
) -> Result<(), Error> {
+ self.check_registration_allowed()?;
self.type_resolver.register_generic_trait::<T>()
}
diff --git a/rust/tests/tests/test_lifecycle_guard.rs
b/rust/tests/tests/test_lifecycle_guard.rs
new file mode 100644
index 000000000..81d4cc7a9
--- /dev/null
+++ b/rust/tests/tests/test_lifecycle_guard.rs
@@ -0,0 +1,273 @@
+// 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.
+
+//! tests for the lifecycle guard that forbids type registrations
+//! after the resolver snapshot has been initialized (i.e after the
+//! first serialize or deserialize call).
+
+use fory_core::error::Error;
+use fory_core::fory::Fory;
+use fory_derive::ForyObject;
+
+/// helper struct used across multiple tests.
+#[derive(ForyObject, Debug, PartialEq)]
+struct Point {
+ x: i32,
+ y: i32,
+}
+
+/// A second type used for late-registration attempts.
+#[derive(ForyObject, Debug, PartialEq)]
+struct Color {
+ r: u8,
+ g: u8,
+ b: u8,
+}
+
+// Postive tests
+#[test]
+fn test_register_before_serialize_succeeds() {
+ let mut fory = Fory::default();
+ // registration before any serialize/deserialize should succeed.
+ assert!(fory.register::<Point>(100).is_ok());
+
+ let point = Point { x: 1, y: 2 };
+ let bytes = fory.serialize(&point).unwrap();
+ let result: Point = fory.deserialize(&bytes).unwrap();
+ assert_eq!(point, result);
+}
+
+#[test]
+fn test_multiple_registrations_before_serialize_succeed() {
+ let mut fory = Fory::default();
+ assert!(fory.register::<Point>(100).is_ok());
+ assert!(fory.register::<Color>(101).is_ok());
+
+ let point = Point { x: 10, y: 20 };
+ let bytes = fory.serialize(&point).unwrap();
+ let result: Point = fory.deserialize(&bytes).unwrap();
+ assert_eq!(point, result);
+}
+
+// Negative tests
+
+/// ensures `register()` is forbidden after `serialize()` triggers snapshot
init.
+#[test]
+fn test_register_after_serialize_fails() {
+ let mut fory = Fory::default();
+ fory.register::<Point>(100).unwrap();
+
+ // first serialize, this initializes the final_type_resolver snapshot.
+ let point = Point { x: 1, y: 2 };
+ let _bytes = fory.serialize(&point).unwrap();
+
+ // now any registration must fail with NotAllowed.
+ let err = fory
+ .register::<Color>(101)
+ .expect_err("register after serialize should fail");
+ assert!(
+ matches!(err, Error::NotAllowed(_)),
+ "expected NotAllowed, got: {:?}",
+ err
+ );
+ let msg = format!("{}", err);
+ assert!(
+ msg.contains("not allowed"),
+ "error message should explain the restriction, got: {}",
+ msg
+ );
+}
+
+/// Ensures `register()` is forbidden after `deserialize()` triggers snapshot
init.
+#[test]
+fn test_register_after_deserialize_fails() {
+ let mut fory = Fory::default();
+ fory.register::<Point>(100).unwrap();
+
+ let point = Point { x: 5, y: 10 };
+ let bytes = fory.serialize(&point).unwrap();
+
+ // Deserialize — also initializes the snapshot if not already done.
+ let _result: Point = fory.deserialize(&bytes).unwrap();
+
+ let err = fory
+ .register::<Color>(101)
+ .expect_err("register after deserialize should fail");
+ assert!(matches!(err, Error::NotAllowed(_)));
+}
+
+/// Ensures `register_by_name()` is forbidden after snapshot init.
+#[test]
+fn test_register_by_name_after_serialize_fails() {
+ let mut fory = Fory::default();
+ fory.register::<Point>(100).unwrap();
+ let _bytes = fory.serialize(&Point { x: 0, y: 0 }).unwrap();
+
+ let err = fory
+ .register_by_name::<Color>("Color")
+ .expect_err("register_by_name after serialize should fail");
+ assert!(matches!(err, Error::NotAllowed(_)));
+}
+
+/// Ensures `register_by_namespace()` is forbidden after snapshot init.
+#[test]
+fn test_register_by_namespace_after_serialize_fails() {
+ let mut fory = Fory::default();
+ fory.register::<Point>(100).unwrap();
+ let _bytes = fory.serialize(&Point { x: 0, y: 0 }).unwrap();
+
+ let err = fory
+ .register_by_namespace::<Color>("com.example", "Color")
+ .expect_err("register_by_namespace after serialize should fail");
+ assert!(matches!(err, Error::NotAllowed(_)));
+}
+
+/// Ensures `register_serializer()` is forbidden after snapshot init.
+#[test]
+fn test_register_serializer_after_serialize_fails() {
+ let mut fory = Fory::default();
+ fory.register::<Point>(100).unwrap();
+ let _bytes = fory.serialize(&Point { x: 0, y: 0 }).unwrap();
+
+ let err = fory
+ .register_serializer::<Color>(102)
+ .expect_err("register_serializer after serialize should fail");
+ assert!(matches!(err, Error::NotAllowed(_)));
+}
+
+/// Ensures `register_serializer_by_name()` is forbidden after snapshot init.
+#[test]
+fn test_register_serializer_by_name_after_serialize_fails() {
+ let mut fory = Fory::default();
+ fory.register::<Point>(100).unwrap();
+ let _bytes = fory.serialize(&Point { x: 0, y: 0 }).unwrap();
+
+ let err = fory
+ .register_serializer_by_name::<Color>("Color")
+ .expect_err("register_serializer_by_name after serialize should fail");
+ assert!(matches!(err, Error::NotAllowed(_)));
+}
+
+/// Ensures `register_serializer_by_namespace()` is forbidden after snapshot
init.
+#[test]
+fn test_register_serializer_by_namespace_after_serialize_fails() {
+ let mut fory = Fory::default();
+ fory.register::<Point>(100).unwrap();
+ let _bytes = fory.serialize(&Point { x: 0, y: 0 }).unwrap();
+
+ let err = fory
+ .register_serializer_by_namespace::<Color>("com.example", "Color")
+ .expect_err("register_serializer_by_namespace after serialize should
fail");
+ assert!(matches!(err, Error::NotAllowed(_)));
+}
+
+/// Ensures `register_generic_trait()` is forbidden after snapshot init.
+#[test]
+fn test_register_generic_trait_after_serialize_fails() {
+ let mut fory = Fory::default();
+ fory.register::<Point>(100).unwrap();
+ let _bytes = fory.serialize(&Point { x: 0, y: 0 }).unwrap();
+
+ let err = fory
+ .register_generic_trait::<Vec<i32>>()
+ .expect_err("register_generic_trait after serialize should fail");
+ assert!(matches!(err, Error::NotAllowed(_)));
+}
+
+/// Ensures `register_union()` is forbidden after snapshot init.
+#[test]
+fn test_register_union_after_serialize_fails() {
+ let mut fory = Fory::default();
+ fory.register::<Point>(100).unwrap();
+ let _bytes = fory.serialize(&Point { x: 0, y: 0 }).unwrap();
+
+ let err = fory
+ .register_union::<Color>(103)
+ .expect_err("register_union after serialize should fail");
+ assert!(matches!(err, Error::NotAllowed(_)));
+}
+
+/// Ensures `register_union_by_name()` is forbidden after snapshot init.
+#[test]
+fn test_register_union_by_name_after_serialize_fails() {
+ let mut fory = Fory::default();
+ fory.register::<Point>(100).unwrap();
+ let _bytes = fory.serialize(&Point { x: 0, y: 0 }).unwrap();
+
+ let err = fory
+ .register_union_by_name::<Color>("Color")
+ .expect_err("register_union_by_name after serialize should fail");
+ assert!(matches!(err, Error::NotAllowed(_)));
+}
+
+/// Ensures `register_union_by_namespace()` is forbidden after snapshot init.
+#[test]
+fn test_register_union_by_namespace_after_serialize_fails() {
+ let mut fory = Fory::default();
+ fory.register::<Point>(100).unwrap();
+ let _bytes = fory.serialize(&Point { x: 0, y: 0 }).unwrap();
+
+ let err = fory
+ .register_union_by_namespace::<Color>("com.example", "Color")
+ .expect_err("register_union_by_namespace after serialize should fail");
+ assert!(matches!(err, Error::NotAllowed(_)));
+}
+
+// Edge-case
+#[test]
+fn test_late_registration_error_message_is_descriptive() {
+ let mut fory = Fory::default();
+ fory.register::<Point>(100).unwrap();
+ let _bytes = fory.serialize(&Point { x: 0, y: 0 }).unwrap();
+
+ let err = fory.register::<Color>(101).unwrap_err();
+ let msg = format!("{}", err);
+ assert!(
+ msg.contains("not allowed"),
+ "should mention 'not allowed', got: {}",
+ msg
+ );
+ assert!(
+ msg.contains("serialize/deserialize"),
+ "should mention serialize/deserialize, got: {}",
+ msg
+ );
+ assert!(
+ msg.contains("finalized"),
+ "should mention the snapshot is finalized, got: {}",
+ msg
+ );
+}
+
+// Positive edge-case
+
+#[test]
+fn test_serialize_multiple_times_after_registration_succeeds() {
+ let mut fory = Fory::default();
+ fory.register::<Point>(100).unwrap();
+
+ let p1 = Point { x: 1, y: 2 };
+ let p2 = Point { x: 3, y: 4 };
+
+ let bytes1 = fory.serialize(&p1).unwrap();
+ let bytes2 = fory.serialize(&p2).unwrap();
+
+ let r1: Point = fory.deserialize(&bytes1).unwrap();
+ let r2: Point = fory.deserialize(&bytes2).unwrap();
+ assert_eq!(p1, r1);
+ assert_eq!(p2, r2);
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]