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/06/20 16:33:02 UTC

(fory) branch main updated: feat: limit remote schema metadata (#3770)

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 fca8af85e feat: limit remote schema metadata (#3770)
fca8af85e is described below

commit fca8af85efc653a6149617b5e6bf4426cc775063
Author: Shawn Yang <sh...@gmail.com>
AuthorDate: Sat Jun 20 22:02:57 2026 +0530

    feat: limit remote schema metadata (#3770)
    
    ## Why?
    
    
    
    ## What does this PR do?
    
    
    
    ## 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?
    
    
    
    - [ ] Does this PR introduce any public API change?
    - [ ] Does this PR introduce any binary protocol compatibility change?
    
    ## Benchmark
---
 .agents/docs-and-formatting.md                     |   5 +
 .agents/languages/dart.md                          |   4 +
 .agents/languages/java.md                          |   9 +
 .agents/languages/javascript.md                    |   2 +
 .agents/repo-reference.md                          |  30 +
 .github/workflows/build-containerized-pr.yml       |   2 +-
 .github/workflows/build-containerized-release.yml  |   2 +-
 .github/workflows/build-native-pr.yml              |   6 +-
 .github/workflows/build-native-release.yml         |   6 +-
 .github/workflows/ci.yml                           |  22 +-
 .github/workflows/release-rust.yaml                |   2 +-
 AGENTS.md                                          |   6 +
 cpp/fory/serialization/config.h                    |  13 +
 cpp/fory/serialization/context.cc                  | 122 ++++-
 cpp/fory/serialization/context.h                   |  19 +-
 cpp/fory/serialization/fory.h                      |  31 ++
 cpp/fory/serialization/namespace_macro_test.cc     |   9 +-
 cpp/fory/serialization/serialization_test.cc       | 231 ++++++++
 cpp/fory/serialization/type_resolver.cc            |  51 +-
 cpp/fory/serialization/type_resolver.h             |  26 +-
 csharp/src/Fory.Generator/ForyModelGenerator.cs    |  60 +-
 csharp/src/Fory/Config.cs                          | 117 +++-
 csharp/src/Fory/Fory.cs                            |   5 +-
 csharp/src/Fory/ReadContext.cs                     | 230 +++++---
 csharp/src/Fory/TypeInfo.cs                        |   9 +
 csharp/src/Fory/TypeMeta.cs                        |  41 +-
 csharp/src/Fory/TypeResolver.cs                    | 240 ++++++--
 csharp/tests/Fory.Tests/ForyRuntimeTests.cs        |  88 ++-
 csharp/tests/Fory.Tests/RuntimeEdgeCaseTests.cs    | 284 +++++++++-
 csharp/tests/Fory.Tests/StringSerializerTests.cs   |   9 +-
 .../fory-test/lib/entity/xlang_test_manual.dart    |   6 +-
 .../test/cross_lang_test/xlang_test_main.dart      |   2 +-
 .../fory/lib/src/codegen/fory_generator.dart       |   2 +-
 .../fory/lib/src/codegen/generated_support.dart    |   2 +-
 dart/packages/fory/lib/src/config.dart             |  34 +-
 .../fory/lib/src/context/read_context.dart         |   6 +-
 .../fory/lib/src/context/write_context.dart        |  32 +-
 dart/packages/fory/lib/src/fory.dart               |  35 +-
 .../packages/fory/lib/src/memory/buffer_mixin.dart |  14 +
 dart/packages/fory/lib/src/meta/type_meta.dart     | 300 ++++------
 .../fory/lib/src/resolver/type_resolver.dart       | 568 +++++++++++--------
 .../lib/src/serializer/collection_serializers.dart |   4 +-
 .../fory/lib/src/serializer/map_serializers.dart   |   4 +-
 .../src/serializer/serialization_field_info.dart   |   4 +-
 .../fory/lib/src/serializer/struct_serializer.dart |  40 +-
 .../fory/test/enum_union_serializer_test.dart      |  22 +
 .../scalar_and_typed_array_serializer_test.dart    |   4 +-
 dart/packages/fory/test/xlang_protocol_test.dart   | 416 ++++++++++++--
 docs/guide/cpp/configuration.md                    |  70 ++-
 docs/guide/csharp/configuration.md                 |  60 +-
 docs/guide/dart/configuration.md                   |  41 +-
 docs/guide/go/configuration.md                     |  56 +-
 docs/guide/java/configuration.md                   |   9 +
 docs/guide/javascript/configuration.md             |  29 +-
 docs/guide/kotlin/configuration.md                 |   5 +
 docs/guide/python/configuration.md                 |  44 +-
 docs/guide/rust/configuration.md                   |  38 +-
 docs/guide/scala/configuration.md                  |   5 +
 docs/guide/swift/configuration.md                  |  25 +-
 docs/guide/xlang/serialization.md                  |  35 ++
 docs/security/deserialization.md                   |  51 ++
 docs/specification/java_serialization_spec.md      |   6 +
 docs/specification/xlang_implementation_guide.md   |  55 +-
 docs/specification/xlang_serialization_spec.md     |  10 +
 go/fory/enum_test.go                               |  97 ++++
 go/fory/fory.go                                    |  57 +-
 go/fory/type_def.go                                |  32 +-
 go/fory/type_def_test.go                           | 189 ++++++-
 go/fory/type_resolver.go                           | 186 +++++--
 .../src/main/java/org/apache/fory/Fory.java        |   4 +-
 .../main/java/org/apache/fory/config/Config.java   |  36 ++
 .../java/org/apache/fory/config/ForyBuilder.java   |  60 ++
 .../org/apache/fory/meta/NativeTypeDefDecoder.java |  49 +-
 .../main/java/org/apache/fory/meta/TypeDef.java    |  19 +-
 .../java/org/apache/fory/meta/TypeDefDecoder.java  |  13 +-
 .../org/apache/fory/resolver/ClassResolver.java    |   2 +-
 .../org/apache/fory/resolver/SharedRegistry.java   | 134 ++++-
 .../org/apache/fory/resolver/TypeResolver.java     | 154 ++++--
 .../fory/serializer/ObjectStreamSerializer.java    |  26 +-
 .../apache/fory/meta/NativeTypeDefEncoderTest.java |  43 ++
 .../org/apache/fory/meta/TypeDefEncoderTest.java   |  44 ++
 .../apache/fory/resolver/ClassResolverTest.java    | 262 ++++++++-
 .../apache/fory/resolver/MetaShareContextTest.java |   2 +-
 .../org/apache/fory/resolver/MetaStringIOTest.java |  24 +-
 .../fory/serializer/LambdaSerializerTest.java      |   2 +-
 .../serializer/ObjectStreamSerializerTest.java     |  33 ++
 .../fory/serializer/UnionSerializerTest.java       |  19 +
 javascript/packages/core/lib/context.ts            | 471 +++++++++++-----
 javascript/packages/core/lib/fory.ts               |  42 ++
 javascript/packages/core/lib/gen/builder.ts        |  12 +-
 javascript/packages/core/lib/gen/enum.ts           |  36 +-
 javascript/packages/core/lib/gen/ext.ts            |   7 +-
 javascript/packages/core/lib/gen/serializer.ts     |   9 +
 javascript/packages/core/lib/gen/struct.ts         |  19 +-
 javascript/packages/core/lib/gen/union.ts          |   7 +-
 javascript/packages/core/lib/meta/TypeMeta.ts      |  40 +-
 javascript/packages/core/lib/type.ts               |   5 +
 javascript/packages/core/package.json              |   1 +
 javascript/packages/core/test/schema-limit.test.js | 554 +++++++++++++++++++
 javascript/test/typemeta.test.ts                   | 182 ++++++-
 javascript/test/union.test.ts                      |  15 +
 python/pyfory/_fory.py                             |  26 +
 python/pyfory/buffer.pxi                           |   2 +-
 python/pyfory/meta/typedef_decoder.py              |  58 +-
 python/pyfory/registry.py                          | 104 +++-
 python/pyfory/serialization.pyx                    |  93 +++-
 python/pyfory/tests/test_typedef_encoding.py       | 303 ++++++++++-
 rust/fory-core/src/buffer.rs                       |   8 +
 rust/fory-core/src/config.rs                       |  36 ++
 rust/fory-core/src/context.rs                      |   4 +-
 rust/fory-core/src/fory.rs                         |  58 +-
 rust/fory-core/src/meta/type_meta.rs               |  48 +-
 rust/fory-core/src/resolver/meta_resolver.rs       | 604 ++++++++++++++++-----
 rust/fory-core/src/serializer/primitive_list.rs    |   6 +-
 rust/tests/tests/test_any.rs                       |   7 +-
 rust/tests/tests/test_enum.rs                      |  22 +
 swift/Sources/Fory/Fory.swift                      |  59 +-
 swift/Sources/Fory/ReadContext.swift               | 221 ++++++--
 swift/Sources/Fory/Serializer.swift                |  18 +-
 swift/Sources/Fory/TypeMeta.swift                  |  38 +-
 swift/Sources/Fory/TypeResolver.swift              | 251 ++++++---
 swift/Sources/Fory/WriteContext.swift              |   7 +-
 swift/Sources/ForyMacro/ForyObjectMacro.swift      | 125 +++++
 .../ForyMacro/ForyObjectMacroReadGeneration.swift  |  17 +-
 .../ForyTests/CollectionSerializerTests.swift      |   8 +-
 swift/Tests/ForyTests/DateTimeTests.swift          |  13 +-
 swift/Tests/ForyTests/EnumTests.swift              |  32 +-
 swift/Tests/ForyTests/ForySwiftTests.swift         | 433 ++++++++++++++-
 swift/Tests/ForyTests/StringSerializerTests.swift  |  14 +-
 swift/Tests/ForyTests/UnsignedTests.swift          |  16 +-
 130 files changed, 7617 insertions(+), 1616 deletions(-)

diff --git a/.agents/docs-and-formatting.md b/.agents/docs-and-formatting.md
index 86b3c7b69..753c2ac81 100644
--- a/.agents/docs-and-formatting.md
+++ b/.agents/docs-and-formatting.md
@@ -29,6 +29,11 @@ Load this file when changing documentation, public APIs, protocol specs, benchma
 - Do not treat checked-in benchmark reports as canonical for current numbers. Run the active harness or a source-aligned size check, and match schema, config, and mode before comparing runtimes.
 - Use portable repo-relative or web links in repository docs, not local filesystem paths. Package readmes rendered by external package managers should link to canonical published docs pages when that is the user-facing context.
 - Published `docs/guide/**` pages should prioritize end-user installation and usage flows unless the file is explicitly a contributor or development guide.
+- Keep `docs/guide/**` configuration text user-action focused. Do not put cache ownership,
+  cold/hot path, publish ordering, exact-local schema bypass, or TypeDef/TypeMeta routing
+  invariants there; put implementation invariants in `docs/specification/xlang_implementation_guide.md`,
+  security classifications in `docs/security/deserialization.md`, and agent-only workflow rules in
+  `.agents/**`.
 - Documentation examples should use normal explicit imports, avoid unused imports, and keep common Fory types unqualified where that is the idiom.
 - Generated Markdown under `docs/benchmarks/**` should satisfy markdownlint blank-line rules at generation time: no repeated blank lines and no extra blank line after final content.
 - Keep default recommendations minimal. Put uncommon optimization or transport patterns in clearly labeled optional sections.
diff --git a/.agents/languages/dart.md b/.agents/languages/dart.md
index 616a4cc0d..f257ece60 100644
--- a/.agents/languages/dart.md
+++ b/.agents/languages/dart.md
@@ -14,6 +14,10 @@ Load this file when changing `dart/`.
 - Keep root numeric wrapper defaults separate from generated field metadata. Root wrapper resolution belongs in the builtin resolver, while annotations and generated metadata choose fixed, tagged, or declared-field encodings.
 - Dart 64-bit carriers are optimized for each platform. Do not replace native extension-type wrappers with allocation-heavy classes or route web/native hot paths through `BigInt` unless the user approves a representation change.
 - In `Buffer`, cursor, serializer, and generated-code hot paths, prefer direct byte/local integer operations and conditional import/export files over callbacks, records, holder objects, wrapper round-trips, or runtime platform branches.
+- Do not add parallel header-low/header-high slot caches or multi-slot recent caches in TypeMeta hot paths to chase benchmark gaps. Header-cache hits must use the concrete checked cache owner directly; if a hit hint is needed, cache one TypeInfo/TypeMeta object and compare the validated header identity on that object, not separate low/high header fields or benchmark-pattern state.
+- If Dart TypeMeta cache ownership changes, keep the invariant in a source comment near the hit path: a checked metadata-cache hit skips the body and must not grow low-bit sentinels, accepted-header fields, parallel header slots, or benchmark-pattern state.
+- Dart expected-type TypeDef reads should compare the expected `TypeInfo` object's cached local TypeDef header before consulting the parsed-metadata map. A match is a direct local-schema hit: skip the remote body, add the expected type to the per-read shared type table, and do not publish to `ParsedTypeMetaCache`, record a remote schema version, or parse/hash the body.
+- Dart local TypeDef construction is a registration-time two-stage process: first record the current `TypeInfo`, then eagerly rebuild local TypeDefs and struct serializers for all registered user types from the complete current registry. Do not move local TypeDef construction back into read/write hot paths or cache-miss workarounds; late registrations must refresh earlier TypeDefs on the registration cold path.
 - Codegen must support private fields through same-library `part` generation. If generated file naming changes from `*.fory.dart`, update builder config, source `part` directives, analysis exclusions, docs, CI snippets, and stale artifacts together.
 - Keep generated Dart outputs (`*.fory.dart`) and Dart `pubspec.lock` files untracked in this repo.
 - For generated numeric or xlang changes, test root values and generated required/nullable fields across schema-consistent and compatible serializers, metadata type IDs, rejection paths, and every affected encoding mode.
diff --git a/.agents/languages/java.md b/.agents/languages/java.md
index 27594ae00..55a3d0691 100644
--- a/.agents/languages/java.md
+++ b/.agents/languages/java.md
@@ -22,6 +22,15 @@ Load this file when changing anything under `java/` or when Java drives a cross-
 - Do not add normal-JVM process-global caches keyed by user classes, generated classes, serializer classes, classloaders, or class-bound method handles. Prefer per-runtime state, immutable shared metadata, or build-time-only template data.
 - Concrete serializers may opt into sharing only after auditing retained fields. Treat serializers retaining `TypeResolver`, `RefResolver`, mutable scratch buffers, runtime state, or classloader-sensitive state as non-shareable unless that state is externalized.
 - Resolver and serializer hot paths should keep the fast-path/null-slow-path shape obvious. Hoist repeated buffer or cache-state access into locals for multi-step operations and keep rebuild/restoration logic cold.
+- Remote metadata and class-token paths that materialize Java classes must keep
+  `TypeResolver.loadClass` or an equivalent owner in the path so
+  `TypeChecker.checkType` and `DisallowedList` run on the remote class name
+  before `Class.forName`.
+  Do not add a direct `Class.forName`, descriptor-load, serializer-lookup, or
+  `loadClassForMeta` bypass that materializes a class from TypeDef/TypeMeta
+  names before those name-level checks. Checks that require `Class<?>`,
+  including `checkClassForDeserialization`, remain after loading; do not replace
+  them with new string-only registration or security checks.
 - Do not use `instanceof` in Java hot paths, including per-value, per-field, per-element,
   read/write/copy, resolver, serializer, codec, and buffer paths. Choose concrete
   implementations during cold setup or code generation, cache final/static-final shape decisions,
diff --git a/.agents/languages/javascript.md b/.agents/languages/javascript.md
index b302673f2..9ebf99d3b 100644
--- a/.agents/languages/javascript.md
+++ b/.agents/languages/javascript.md
@@ -9,6 +9,8 @@ Load this file when changing `javascript/`.
 - IDL Jest tests should stay local to generated-code construction and local `Fory` serialize/deserialize assertions. Java-driven peer orchestration belongs in the existing integration harness.
 - Language peer entrypoints under `integration_tests/idl_tests` should mirror existing peers and validate semantic equality, not just deserialize and reserialize bytes.
 - Preserve generated serializer hot paths that bind writer, reader, ref, resolver, and metadata locals in outer closures; do not replace them with per-call context lookups without a measured reason.
+- Do not add parallel header-low/header-high slot caches in TypeMeta hot paths to chase benchmark gaps. Header-cache hits must use the concrete checked cache owner directly; if a small hit hint is needed, cache TypeMeta objects themselves and compare `TypeMeta.headerHash`, not separate low/high header fields or benchmark-pattern state.
+- JavaScript TypeMeta header cache hits should compare the 52-bit TypeMeta header hash directly. The hash is precise in JS `Number` and already includes the low header bits as hash input; do not add extra low-bit fields, sentinel state, nullable accepted headers, or parallel slot arrays around it.
 - Runtime value carriers such as decimal or reduced-precision numeric types belong under the core `types/` ownership boundary, with imports, exports, and codegen externals updated together.
 - Keep `TypeInfo` as schema metadata. Compatibility-sensitive decisions belong on `TypeResolver` or explicit operations, not as retained resolver state on metadata objects.
 - Normalize optional boolean config values at config construction; do not carry `null` through runtime paths when it means `false`.
diff --git a/.agents/repo-reference.md b/.agents/repo-reference.md
index 8df1d30b8..4d420b1f0 100644
--- a/.agents/repo-reference.md
+++ b/.agents/repo-reference.md
@@ -49,6 +49,36 @@ Apache Fory is a multi-language serialization framework with multiple wire forma
   - `foryc schema.fdl --lang <langs> --output <dir>`
 - Never edit generated code manually. Update the source schema or IDL and regenerate.
 - Protocol changes must update `docs/specification/**` and the relevant cross-language tests.
+- Remote `TypeDef` or `TypeMeta` schema limits are resource controls on cold metadata cache-miss
+  parse/publish paths only. They must not change wire format, registration, dynamic type loading,
+  unknown-type behavior, deserialization policy, schema-evolution semantics, or metadata
+  cache-hit/generated-reader hot paths. Count a remote metadata version only after the required read
+  state has been successfully built and the owning metadata cache can publish it; failed or
+  incompatible metadata must not consume the limit. Struct types may have multiple schema-evolution
+  versions; compatible named enum/ext/union metadata normally has one version but still counts
+  against remote metadata total limits when it is sent as shared metadata. Pure id-based enum, ext,
+  and typed-union values use type id plus user type id and must not be moved onto this metadata
+  cache path. Exact-local metadata bypass is not struct-only: after the existing type and
+  deserialization-policy checks for the selected local type, compare the original received encoded
+  bytes with the local encoded metadata bytes, and allow exact matches for struct and named
+  enum/ext/union metadata without consuming remote schema-version limits. Derive the local
+  exact-match candidate inside the metadata owner from the decoded `userTypeId` or
+  `(namespace, typeName)` identity; do not add caller-threaded expected-type parameters only for
+  this check in any runtime. Java and Python may lazy-build local metadata bytes only after this
+  identity lookup selects a local class and the existing class, registration, and deserialization
+  policy checks have run. When a typed read path already has a declared local type and validates the
+  decoded metadata identity against it, use that same local type for exact-local byte comparison and
+  cache publish; dynamic or Any paths resolve the local type from decoded metadata. A runtime may
+  also skip a received metadata body when the current declared type already owns an identical local
+  metadata header; that is a local-schema hit, not a remote cache publish, and it must not consume
+  schema-version limits. Other header-only skips are allowed only after the same owning remote
+  metadata cache has validated a previous body for that header.
+- Remote metadata body and struct field-count limits are also cold-path resource controls.
+  `maxTypeMetaBytes` limits one received TypeDef or TypeMeta body excluding the 8-byte header and
+  extended-size varint; `maxTypeFields` limits one received struct metadata body's field count
+  (Java native TypeDef counts total fields across class layers). Check these before body
+  copy/decompression and before field-list allocation, and never add cache-hit or generated-reader
+  hot-path work for them.
 
 ## Runtime Map
 
diff --git a/.github/workflows/build-containerized-pr.yml b/.github/workflows/build-containerized-pr.yml
index 05b70357e..fda2c28b9 100644
--- a/.github/workflows/build-containerized-pr.yml
+++ b/.github/workflows/build-containerized-pr.yml
@@ -33,7 +33,7 @@ jobs:
     steps:
       - uses: actions/checkout@v5
       - name: Set up Bazel
-        uses: bazel-contrib/setup-bazel@083175551ceeceebc757ebee2127fde78840ca77 # 0.18.0
+        uses: bazel-contrib/setup-bazel@c5acdfb288317d0b5c0bbd7a396a3dc868bb0f86 # 0.19.0
         with:
           bazelisk-cache: true
           bazelisk-version: '1.x'
diff --git a/.github/workflows/build-containerized-release.yml b/.github/workflows/build-containerized-release.yml
index 49b658ab5..51eb67c44 100644
--- a/.github/workflows/build-containerized-release.yml
+++ b/.github/workflows/build-containerized-release.yml
@@ -36,7 +36,7 @@ jobs:
     steps:
       - uses: actions/checkout@v5
       - name: Set up Bazel
-        uses: bazel-contrib/setup-bazel@083175551ceeceebc757ebee2127fde78840ca77 # 0.18.0
+        uses: bazel-contrib/setup-bazel@c5acdfb288317d0b5c0bbd7a396a3dc868bb0f86 # 0.19.0
         with:
           bazelisk-cache: true
           bazelisk-version: '1.x'
diff --git a/.github/workflows/build-native-pr.yml b/.github/workflows/build-native-pr.yml
index 0b216e6b5..d62da223c 100644
--- a/.github/workflows/build-native-pr.yml
+++ b/.github/workflows/build-native-pr.yml
@@ -36,7 +36,7 @@ jobs:
           python-version: ${{ matrix.python-version }}
           cache: 'pip'
       - name: Set up Bazel
-        uses: bazel-contrib/setup-bazel@083175551ceeceebc757ebee2127fde78840ca77 # 0.18.0
+        uses: bazel-contrib/setup-bazel@c5acdfb288317d0b5c0bbd7a396a3dc868bb0f86 # 0.19.0
         with:
           bazelisk-cache: true
           bazelisk-version: '1.x'
@@ -67,7 +67,7 @@ jobs:
           python-version: ${{ matrix.python-version }}
           cache: 'pip'
       - name: Set up Bazel
-        uses: bazel-contrib/setup-bazel@083175551ceeceebc757ebee2127fde78840ca77 # 0.18.0
+        uses: bazel-contrib/setup-bazel@c5acdfb288317d0b5c0bbd7a396a3dc868bb0f86 # 0.19.0
         with:
           bazelisk-cache: true
           bazelisk-version: '1.x'
@@ -101,7 +101,7 @@ jobs:
           python-version: ${{ matrix.python-version }}
           cache: 'pip'
       - name: Set up Bazel
-        uses: bazel-contrib/setup-bazel@083175551ceeceebc757ebee2127fde78840ca77 # 0.18.0
+        uses: bazel-contrib/setup-bazel@c5acdfb288317d0b5c0bbd7a396a3dc868bb0f86 # 0.19.0
         with:
           bazelisk-cache: true
           bazelisk-version: '1.x'
diff --git a/.github/workflows/build-native-release.yml b/.github/workflows/build-native-release.yml
index 0ff3059c8..60d7aee6f 100644
--- a/.github/workflows/build-native-release.yml
+++ b/.github/workflows/build-native-release.yml
@@ -37,7 +37,7 @@ jobs:
           python-version: ${{ matrix.python-version }}
           cache: 'pip'
       - name: Set up Bazel
-        uses: bazel-contrib/setup-bazel@083175551ceeceebc757ebee2127fde78840ca77 # 0.18.0
+        uses: bazel-contrib/setup-bazel@c5acdfb288317d0b5c0bbd7a396a3dc868bb0f86 # 0.19.0
         with:
           bazelisk-cache: true
           bazelisk-version: '1.x'
@@ -82,7 +82,7 @@ jobs:
           python-version: ${{ matrix.python-version }}
           cache: 'pip'
       - name: Set up Bazel
-        uses: bazel-contrib/setup-bazel@083175551ceeceebc757ebee2127fde78840ca77 # 0.18.0
+        uses: bazel-contrib/setup-bazel@c5acdfb288317d0b5c0bbd7a396a3dc868bb0f86 # 0.19.0
         with:
           bazelisk-cache: true
           bazelisk-version: '1.x'
@@ -129,7 +129,7 @@ jobs:
           python-version: ${{ matrix.python-version }}
           cache: 'pip'
       - name: Set up Bazel
-        uses: bazel-contrib/setup-bazel@083175551ceeceebc757ebee2127fde78840ca77 # 0.18.0
+        uses: bazel-contrib/setup-bazel@c5acdfb288317d0b5c0bbd7a396a3dc868bb0f86 # 0.19.0
         with:
           bazelisk-cache: true
           bazelisk-version: '1.x'
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 287b9212b..1a9ee4dd5 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -795,7 +795,7 @@ jobs:
           restore-keys: |
             ${{ runner.os }}-maven-
       - name: Cache Cargo dependencies
-        uses: Swatinem/rust-cache@v2
+        uses: swatinem/rust-cache@v2
         with:
           workspaces: integration_tests/grpc_tests/rust
           cache-bin: "false"
@@ -959,7 +959,7 @@ jobs:
           restore-keys: |
             ${{ runner.os }}-maven-
       - name: Cache Cargo dependencies
-        uses: Swatinem/rust-cache@v2
+        uses: swatinem/rust-cache@v2
         with:
           workspaces: rust
           cache-bin: "false"
@@ -988,7 +988,7 @@ jobs:
           restore-keys: |
             ${{ runner.os }}-maven-
       - name: Cache Cargo dependencies
-        uses: Swatinem/rust-cache@v2
+        uses: swatinem/rust-cache@v2
         with:
           workspaces: rust
           cache-bin: "false"
@@ -1018,7 +1018,7 @@ jobs:
           python-version: 3.11
           cache: "pip"
       - name: Set up Bazel
-        uses: bazel-contrib/setup-bazel@083175551ceeceebc757ebee2127fde78840ca77 # 0.18.0
+        uses: bazel-contrib/setup-bazel@c5acdfb288317d0b5c0bbd7a396a3dc868bb0f86 # 0.19.0
         with:
           bazelisk-cache: true
           bazelisk-version: "1.x"
@@ -1072,7 +1072,7 @@ jobs:
           python-version: 3.11
           cache: "pip"
       - name: Set up Bazel
-        uses: bazel-contrib/setup-bazel@083175551ceeceebc757ebee2127fde78840ca77 # 0.18.0
+        uses: bazel-contrib/setup-bazel@c5acdfb288317d0b5c0bbd7a396a3dc868bb0f86 # 0.19.0
         with:
           bazelisk-cache: true
           bazelisk-version: "1.x"
@@ -1122,7 +1122,7 @@ jobs:
           python-version: 3.11
           cache: "pip"
       - name: Set up Bazel
-        uses: bazel-contrib/setup-bazel@083175551ceeceebc757ebee2127fde78840ca77 # 0.18.0
+        uses: bazel-contrib/setup-bazel@c5acdfb288317d0b5c0bbd7a396a3dc868bb0f86 # 0.19.0
         with:
           bazelisk-cache: true
           bazelisk-version: "1.x"
@@ -1202,7 +1202,7 @@ jobs:
           restore-keys: |
             ${{ runner.os }}-maven-
       - name: Set up Bazel
-        uses: bazel-contrib/setup-bazel@083175551ceeceebc757ebee2127fde78840ca77 # 0.18.0
+        uses: bazel-contrib/setup-bazel@c5acdfb288317d0b5c0bbd7a396a3dc868bb0f86 # 0.19.0
         with:
           bazelisk-cache: true
           bazelisk-version: "1.x"
@@ -1266,7 +1266,7 @@ jobs:
         with:
           python-version: "3.11"
       - name: Set up Bazel
-        uses: bazel-contrib/setup-bazel@083175551ceeceebc757ebee2127fde78840ca77 # 0.18.0
+        uses: bazel-contrib/setup-bazel@c5acdfb288317d0b5c0bbd7a396a3dc868bb0f86 # 0.19.0
         with:
           bazelisk-cache: true
           bazelisk-version: "1.x"
@@ -1304,7 +1304,7 @@ jobs:
           python-version: ${{ matrix.python-version }}
           cache: "pip"
       - name: Set up Bazel
-        uses: bazel-contrib/setup-bazel@083175551ceeceebc757ebee2127fde78840ca77 # 0.18.0
+        uses: bazel-contrib/setup-bazel@c5acdfb288317d0b5c0bbd7a396a3dc868bb0f86 # 0.19.0
         with:
           bazelisk-cache: true
           bazelisk-version: "1.x"
@@ -1345,7 +1345,7 @@ jobs:
           python-version: ${{ matrix.python-version }}
           cache: "pip"
       - name: Set up Bazel
-        uses: bazel-contrib/setup-bazel@083175551ceeceebc757ebee2127fde78840ca77 # 0.18.0
+        uses: bazel-contrib/setup-bazel@c5acdfb288317d0b5c0bbd7a396a3dc868bb0f86 # 0.19.0
         with:
           bazelisk-cache: true
           bazelisk-version: "1.x"
@@ -1423,7 +1423,7 @@ jobs:
           python-version: 3.8
           cache: "pip"
       - name: Set up Bazel
-        uses: bazel-contrib/setup-bazel@083175551ceeceebc757ebee2127fde78840ca77 # 0.18.0
+        uses: bazel-contrib/setup-bazel@c5acdfb288317d0b5c0bbd7a396a3dc868bb0f86 # 0.19.0
         with:
           bazelisk-cache: true
           bazelisk-version: "1.x"
diff --git a/.github/workflows/release-rust.yaml b/.github/workflows/release-rust.yaml
index 0930a1d2d..9f1153bb4 100644
--- a/.github/workflows/release-rust.yaml
+++ b/.github/workflows/release-rust.yaml
@@ -45,7 +45,7 @@ jobs:
 
       - name: Authenticate to crates.io
         id: crates-io-auth
-        uses: rust-lang/crates-io-auth-action@b7e9a28eded4986ec6b1fa40eeee8f8f165559ec
+        uses: rust-lang/crates-io-auth-action@bbd81622f20ce9e2dd9622e3218b975523e45bbe # v1.0.4
 
       - name: Export crates.io token
         shell: bash
diff --git a/AGENTS.md b/AGENTS.md
index 02e44f69a..830db1f56 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -28,7 +28,10 @@ This is the entry point for AI guidance in Apache Fory. Read this file first, th
 - Preserve architecture. Do not introduce new layers, parallel flows, or public APIs unless explicitly requested; prefer local repair in the existing owner over shared-infra expansion, and stop if a fix conflicts with an ADR, spec, or invariant.
 - Respect ownership. Keep logic, state, and helpers in their natural owner, and do not move serializer-local, context-local, runtime-type-local, or protocol-local problems into global utilities.
 - Check the spec before implementation. For wire behavior and xlang mapping, use the specs as the source of truth and never copy one runtime's bug into another runtime just to make tests pass.
+- Do not make assumptions about runtime behavior, ownership, registration, metadata construction, protocol semantics, or test coverage. Read the current code, owning docs/specs, and relevant tests before making a design judgment or implementation decision. If the evidence is incomplete, inspect more or state the uncertainty explicitly instead of filling gaps from memory or analogy with another runtime.
 - For untrusted deserialization, read `docs/security/deserialization.md` before changing allocation, stream filling, skip, reference, metadata, or policy validation behavior. Variable-length deserialization must not allocate or reserve from attacker-declared lengths or counts before the byte owner has proven proportional readable bytes with `checkReadableBytes` or the runtime equivalent.
+- For remote TypeDef/TypeMeta reads, the checked metadata cache is the only owner of remote "already validated" state. Cache hit means the header was previously parsed, body/hash-validated, policy-checked, and published by that cache, so the hot path must skip the body and use cached metadata without extra validation, hashing, limit checks, exact-local checks, allocation, or policy work. A known expected local TypeDef/TypeMeta header/hash match is a local-schema hit, not a remote cache m [...]
+- When a user corrects a non-obvious invariant, encode it in the nearest source comment before continuing, and also update `AGENTS.md`, `.agents/**`, docs, or specs when the rule is reusable beyond one file. Do not rely only on chat history, task notes, commit messages, or benchmark logs for corrections that protect security, protocol behavior, ownership, naming, or hot-path performance.
 - Reject semantic hacks. Do not bypass broken semantics by deleting cases, simplifying callers, adding coercion hooks, or using workaround fallbacks; fix the underlying bug and prove it with focused tests.
 - Protect hot paths. Avoid per-call allocations, callback objects, result tuples or records, unnecessary runtime branches, and wrapper-class substitutions in hot codec/runtime paths; prefer conditional imports and allocation-free concrete implementations where they fit the language.
 - Keep public APIs minimal. Public APIs must match user ownership and mental model, not internal implementation details; generated flows stay type-owned, while manual serializer registration stays explicit.
@@ -145,6 +148,9 @@ This is the entry point for AI guidance in Apache Fory. Read this file first, th
 - If Swift xlang behavior changes, run `org.apache.fory.xlang.SwiftXlangTest` too.
 - For performance regressions or optimizations, profile or otherwise measure the current branch and a fresh `apache/main` baseline before changing code; optimize the measured hotspot, not guessed code.
 - When comparing benchmark results against `apache/main`, use a separate sibling worktree named `fory-benchmark-baseline` by default. Before creating a new worktree, check whether `../fory-benchmark-baseline` already exists and reuse it to avoid repeated benchmark dependency rebuilds. Always fetch and sync that baseline worktree to the latest `apache/main` before measuring it, and store benchmark result files under that worktree so older runs remain available as reference data. Treat sto [...]
+- Before benchmarking a checked-out version, install or build the required Fory packages for that version, such as the Java artifacts, Python package, and the target runtime package needed by the benchmark.
+- Run and close old/new benchmark comparisons for exactly one language at a time. If that language has a slowdown greater than 1%, keep working only on that language until the slowdown is within 1% before moving to the next language.
+- Treat a same-benchmark slowdown greater than 1% as unresolved until the retained median is within 1% of the baseline. Faster results are acceptable only after verifying that generated code, benchmark shape, safety checks, and protocol semantics did not skip required work. Do not add artificial slowdowns or benchmark-shape changes to force a match.
 - Do not change protocol behavior, benchmark payloads, or public APIs solely to manufacture performance wins.
 - For performance work, run the relevant benchmark immediately after each change and report the command plus before/after numbers.
 - For performance-optimization rounds, append the hypothesis, change, benchmark command, before/after numbers, and keep/revert decision to `tasks/perf_optimization_rounds.md`.
diff --git a/cpp/fory/serialization/config.h b/cpp/fory/serialization/config.h
index d471c3907..eac9c1443 100644
--- a/cpp/fory/serialization/config.h
+++ b/cpp/fory/serialization/config.h
@@ -52,6 +52,19 @@ struct Config {
   /// When enabled, avoids duplicating shared objects and handles cycles.
   bool track_ref = true;
 
+  /// Maximum accepted field count in one received struct TypeMeta.
+  uint32_t max_type_fields = 512;
+
+  /// Maximum accepted body size in one received TypeMeta.
+  uint32_t max_type_meta_bytes = 4096;
+
+  /// Maximum accepted remote metadata versions for one logical type.
+  uint32_t max_schema_versions_per_type = 10;
+
+  /// Maximum accepted average remote metadata versions across logical
+  /// types. The effective global minimum remains 8192 schemas.
+  uint32_t max_average_schema_versions_per_type = 3;
+
   /// Default constructor with sensible defaults
   Config() = default;
 };
diff --git a/cpp/fory/serialization/context.cc b/cpp/fory/serialization/context.cc
index 95b245320..deff5ee16 100644
--- a/cpp/fory/serialization/context.cc
+++ b/cpp/fory/serialization/context.cc
@@ -22,6 +22,8 @@
 #include "fory/serialization/type_resolver.h"
 #include "fory/thirdparty/MurmurHash3.h"
 #include "fory/type/type.h"
+#include <algorithm>
+#include <cstring>
 
 namespace fory {
 namespace serialization {
@@ -477,8 +479,59 @@ ReadContext::read_enum_type_info(uint32_t base_type_id) {
   return Unexpected(Error::type_mismatch(type_id, base_type_id));
 }
 
-// Maximum number of parsed type defs to cache (avoid OOM from malicious input)
-static constexpr size_t k_max_parsed_num_type_defs = 8192;
+static constexpr size_t k_min_remote_type_meta_limit = 8192;
+
+Result<std::string, Error>
+ReadContext::check_remote_type_meta_limit(const TypeMeta &type_meta) {
+  std::string key;
+  if (type_meta.register_by_name) {
+    key.reserve(type_meta.namespace_str.size() + type_meta.type_name.size() +
+                2);
+    key.push_back('n');
+    key.append(type_meta.namespace_str);
+    key.push_back('\0');
+    key.append(type_meta.type_name);
+  } else {
+    key = "i" + std::to_string(type_meta.user_type_id);
+  }
+
+  auto *entry = remote_schema_versions_by_type_.find(key);
+  const uint32_t versions_for_type = entry == nullptr ? 0 : entry->second;
+  if (FORY_PREDICT_FALSE(versions_for_type >=
+                         config_->max_schema_versions_per_type)) {
+    return Unexpected(Error::invalid_data(
+        "Remote schema version limit exceeded for one type. The data may be "
+        "malicious. If the data is not malicious, please increase "
+        "max_schema_versions_per_type=" +
+        std::to_string(config_->max_schema_versions_per_type)));
+  }
+
+  const size_t accepted_type_count =
+      remote_schema_versions_by_type_.size() + (entry == nullptr ? 1 : 0);
+  const size_t global_limit = std::max(
+      k_min_remote_type_meta_limit,
+      accepted_type_count *
+          static_cast<size_t>(config_->max_average_schema_versions_per_type));
+  if (FORY_PREDICT_FALSE(total_accepted_schema_versions_ >= global_limit)) {
+    return Unexpected(Error::invalid_data(
+        "Remote schema version limit exceeded globally. The data may be "
+        "malicious. If the data is not malicious, please increase "
+        "max_average_schema_versions_per_type=" +
+        std::to_string(config_->max_average_schema_versions_per_type)));
+  }
+
+  return key;
+}
+
+void ReadContext::record_remote_type_meta(const std::string &type_key) {
+  auto *entry = remote_schema_versions_by_type_.find(type_key);
+  if (entry == nullptr) {
+    remote_schema_versions_by_type_[type_key] = 1;
+  } else {
+    ++entry->second;
+  }
+  ++total_accepted_schema_versions_;
+}
 
 Result<const TypeInfo *, Error> ReadContext::read_type_meta() {
   Error error;
@@ -504,11 +557,12 @@ Result<const TypeInfo *, Error> ReadContext::read_type_meta() {
   }
 
   // Check if we already parsed this type meta (cache lookup by header)
-  if (has_last_meta_header_ && meta_header == last_meta_header_) {
+  if (has_cached_meta_header_ && meta_header == cached_meta_header_) {
     // Header-cache hits intentionally skip without rehashing. Entries reach
     // this cache only after a successful TypeMeta parse and 52-bit
-    // metadata-hash validation.
-    const TypeInfo *cached = last_meta_type_info_;
+    // metadata-hash validation. Do not add body/hash/schema-limit/exact-local
+    // checks here; the miss path owns them before publish.
+    const TypeInfo *cached = cached_meta_type_info_;
     reading_type_infos_.push_back(cached);
     FORY_RETURN_NOT_OK(
         TypeMeta::skip_bytes_for_validated_header(*buffer_, meta_header));
@@ -519,20 +573,25 @@ Result<const TypeInfo *, Error> ReadContext::read_type_meta() {
   if (cache_entry != nullptr) {
     // Header-cache hits intentionally skip without rehashing. Entries reach
     // this cache only after a successful TypeMeta parse and 52-bit
-    // metadata-hash validation.
+    // metadata-hash validation. Do not add body/hash/schema-limit/exact-local
+    // checks here; the miss path owns them before publish.
     const TypeInfo *cached = cache_entry->second;
     reading_type_infos_.push_back(cached);
-    has_last_meta_header_ = true;
-    last_meta_header_ = meta_header;
-    last_meta_type_info_ = cached;
+    has_cached_meta_header_ = true;
+    cached_meta_header_ = meta_header;
+    cached_meta_type_info_ = cached;
     FORY_RETURN_NOT_OK(
         TypeMeta::skip_bytes_for_validated_header(*buffer_, meta_header));
     return cached;
   }
 
   // Not in cache - parse the TypeMeta
-  FORY_TRY(parsed_meta,
-           TypeMeta::from_bytes_with_header(*buffer_, meta_header));
+  const uint32_t type_def_start =
+      buffer_->reader_index() - static_cast<uint32_t>(sizeof(int64_t));
+  FORY_TRY(parsed_meta, TypeMeta::from_bytes_with_header(
+                            *buffer_, meta_header, config_->max_type_fields,
+                            config_->max_type_meta_bytes));
+  const uint32_t type_def_end = buffer_->reader_index();
 
   // Find local TypeInfo to get field_id mapping (optional for schema evolution)
   const TypeInfo *local_type_info = nullptr;
@@ -557,6 +616,24 @@ Result<const TypeInfo *, Error> ReadContext::read_type_meta() {
     }
   }
 
+  if (local_type_info) {
+    const auto &local_type_def = local_type_info->type_def;
+    const size_t remote_type_def_size =
+        static_cast<size_t>(type_def_end - type_def_start);
+    if (local_type_def.size() == remote_type_def_size &&
+        std::memcmp(local_type_def.data(), buffer_->data() + type_def_start,
+                    remote_type_def_size) == 0) {
+      parsed_type_infos_[meta_header] = local_type_info;
+      has_cached_meta_header_ = true;
+      cached_meta_header_ = meta_header;
+      cached_meta_type_info_ = local_type_info;
+      reading_type_infos_.push_back(local_type_info);
+      return local_type_info;
+    }
+  }
+
+  FORY_TRY(remote_schema_key, check_remote_type_meta_limit(*parsed_meta));
+
   // Create TypeInfo with field_ids assigned
   auto type_info = std::make_unique<TypeInfo>();
   if (local_type_info) {
@@ -583,21 +660,13 @@ Result<const TypeInfo *, Error> ReadContext::read_type_meta() {
     type_info->type_meta = std::move(parsed_meta);
   }
 
-  // get raw pointer before moving into storage
-  const TypeInfo *raw_ptr = type_info.get();
-
-  // Store in primary storage
-  if (parsed_type_infos_.size() < k_max_parsed_num_type_defs) {
-    cached_type_infos_.push_back(std::move(type_info));
-    raw_ptr = cached_type_infos_.back().get();
-    parsed_type_infos_[meta_header] = raw_ptr;
-    has_last_meta_header_ = true;
-    last_meta_header_ = meta_header;
-    last_meta_type_info_ = raw_ptr;
-  } else {
-    owned_reading_type_infos_.push_back(std::move(type_info));
-    raw_ptr = owned_reading_type_infos_.back().get();
-  }
+  cached_type_infos_.push_back(std::move(type_info));
+  const TypeInfo *raw_ptr = cached_type_infos_.back().get();
+  parsed_type_infos_[meta_header] = raw_ptr;
+  has_cached_meta_header_ = true;
+  cached_meta_header_ = meta_header;
+  cached_meta_type_info_ = raw_ptr;
+  record_remote_type_meta(remote_schema_key);
 
   reading_type_infos_.push_back(raw_ptr);
   return raw_ptr;
@@ -677,7 +746,6 @@ void ReadContext::reset() {
     ref_reader_.reset();
   }
   reading_type_infos_.clear();
-  owned_reading_type_infos_.clear();
   current_dyn_depth_ = 0;
   if (meta_string_table_active_) {
     meta_string_table_.reset();
diff --git a/cpp/fory/serialization/context.h b/cpp/fory/serialization/context.h
index 60e7974d0..6af99c4cc 100644
--- a/cpp/fory/serialization/context.h
+++ b/cpp/fory/serialization/context.h
@@ -28,9 +28,11 @@
 #include "fory/util/buffer.h"
 #include "fory/util/error.h"
 #include "fory/util/flat_int_map.h"
+#include "fory/util/macros.h"
 #include "fory/util/result.h"
 
 #include <cassert>
+#include <string>
 #include <typeindex>
 
 namespace fory {
@@ -39,6 +41,7 @@ namespace serialization {
 // Forward declarations
 class TypeResolver;
 class ReadContext;
+class TypeMeta;
 
 /// RAII helper to automatically decrease dynamic depth when leaving scope.
 /// Used for tracking nested polymorphic type deserialization depth.
@@ -656,6 +659,10 @@ public:
   inline const Config &config() const { return *config_; }
 
 private:
+  FORY_NOINLINE Result<std::string, Error>
+  check_remote_type_meta_limit(const TypeMeta &type_meta);
+  void record_remote_type_meta(const std::string &type_key);
+
   // Error state - accumulated during deserialization, checked at the end
   Error error_;
 
@@ -666,22 +673,22 @@ private:
   uint32_t current_dyn_depth_;
 
   // Meta sharing state (for compatible mode)
-  // Per-message storage for TypeInfo objects not cached across messages.
-  std::vector<std::unique_ptr<TypeInfo>> owned_reading_type_infos_;
   // Persistent cache storage for TypeInfo objects keyed by meta header.
   std::vector<std::unique_ptr<TypeInfo>> cached_type_infos_;
-  // Index-based access (pointers to owned_reading_type_infos_ or type_resolver)
+  // Index-based access (pointers to cached_type_infos_ or type_resolver)
   std::vector<const TypeInfo *> reading_type_infos_;
   // Cache by meta_header (pointers to cached_type_infos_)
   fory::flat_hash_map<int64_t, const TypeInfo *> parsed_type_infos_;
   // Fast path for repeated type meta headers.
-  int64_t last_meta_header_ = 0;
-  const TypeInfo *last_meta_type_info_ = nullptr;
-  bool has_last_meta_header_ = false;
+  int64_t cached_meta_header_ = 0;
+  const TypeInfo *cached_meta_type_info_ = nullptr;
+  bool has_cached_meta_header_ = false;
   bool meta_string_table_active_ = false;
 
   // Dynamic meta strings used for named type/class info.
   meta::MetaStringTable meta_string_table_;
+  fory::flat_hash_map<std::string, uint32_t> remote_schema_versions_by_type_;
+  size_t total_accepted_schema_versions_ = 0;
 };
 
 /// Implementation of DynDepthGuard destructor
diff --git a/cpp/fory/serialization/fory.h b/cpp/fory/serialization/fory.h
index 3361cb621..36ef992d1 100644
--- a/cpp/fory/serialization/fory.h
+++ b/cpp/fory/serialization/fory.h
@@ -109,6 +109,37 @@ public:
     return *this;
   }
 
+  /// Set maximum accepted field count in one received struct TypeMeta.
+  ForyBuilder &max_type_fields(uint32_t max_fields) {
+    FORY_CHECK(max_fields > 0) << "max_type_fields must be positive";
+    config_.max_type_fields = max_fields;
+    return *this;
+  }
+
+  /// Set maximum accepted body size in one received TypeMeta.
+  ForyBuilder &max_type_meta_bytes(uint32_t max_bytes) {
+    FORY_CHECK(max_bytes > 0) << "max_type_meta_bytes must be positive";
+    config_.max_type_meta_bytes = max_bytes;
+    return *this;
+  }
+
+  /// Set maximum accepted remote metadata versions for one logical type.
+  ForyBuilder &max_schema_versions_per_type(uint32_t max_versions) {
+    FORY_CHECK(max_versions > 0)
+        << "max_schema_versions_per_type must be positive";
+    config_.max_schema_versions_per_type = max_versions;
+    return *this;
+  }
+
+  /// Set maximum accepted average remote metadata versions across logical
+  /// types. The effective global minimum remains 8192 schemas.
+  ForyBuilder &max_average_schema_versions_per_type(uint32_t max_versions) {
+    FORY_CHECK(max_versions > 0)
+        << "max_average_schema_versions_per_type must be positive";
+    config_.max_average_schema_versions_per_type = max_versions;
+    return *this;
+  }
+
   /// Provide a custom type resolver instance.
   ForyBuilder &type_resolver(std::shared_ptr<TypeResolver> resolver) {
     type_resolver_ = std::move(resolver);
diff --git a/cpp/fory/serialization/namespace_macro_test.cc b/cpp/fory/serialization/namespace_macro_test.cc
index d57fe775b..e5299dc47 100644
--- a/cpp/fory/serialization/namespace_macro_test.cc
+++ b/cpp/fory/serialization/namespace_macro_test.cc
@@ -192,8 +192,13 @@ TEST(NamespaceMacros, FieldConfigInNamespace) {
 }
 
 TEST(NamespaceMacros, UnionInNamespace) {
-  auto fory =
-      Fory::builder().xlang(true).compatible(false).track_ref(false).build();
+  auto fory = Fory::builder()
+                  .xlang(true)
+                  .compatible(true)
+                  .track_ref(false)
+                  .max_type_meta_bytes(1)
+                  .max_schema_versions_per_type(1)
+                  .build();
   ASSERT_TRUE(fory.register_union<macro_test::Choice>(1001).ok());
 
   auto bytes = fory.serialize(macro_test::Choice::text("hello"));
diff --git a/cpp/fory/serialization/serialization_test.cc b/cpp/fory/serialization/serialization_test.cc
index 840a522a6..8bed168d6 100644
--- a/cpp/fory/serialization/serialization_test.cc
+++ b/cpp/fory/serialization/serialization_test.cc
@@ -31,6 +31,7 @@
 #include <map>
 #include <string>
 #include <thread>
+#include <variant>
 #include <vector>
 
 #include "fory/type/type.h"
@@ -89,6 +90,16 @@ struct ExtWithDestructor {
   FORY_STRUCT(ExtWithDestructor, value);
 };
 
+struct IdLimitExt {
+  int32_t value = 0;
+
+  bool operator==(const IdLimitExt &other) const {
+    return value == other.value;
+  }
+
+  FORY_STRUCT(IdLimitExt, value);
+};
+
 namespace fory {
 namespace serialization {
 namespace test {
@@ -957,6 +968,180 @@ TEST(SerializationTest, RegistrationByNameFailureDoesNotLeakTypeInfo) {
   EXPECT_EQ(dotted_type_name.error().code(), ErrorCode::Invalid);
 }
 
+static std::vector<uint8_t> make_remote_type_meta(const std::string &type_name,
+                                                  const std::string &field) {
+  std::vector<FieldInfo> fields;
+  fields.emplace_back(
+      field, FieldType(static_cast<uint32_t>(TypeId::VARINT32), false));
+  TypeMeta meta =
+      TypeMeta::from_fields(static_cast<uint32_t>(TypeId::NAMED_STRUCT),
+                            "example", type_name, true, 0, std::move(fields));
+  auto bytes = meta.to_bytes();
+  EXPECT_TRUE(bytes.ok()) << "TypeMeta serialization failed: "
+                          << bytes.error().to_string();
+  return bytes.value();
+}
+
+static std::vector<uint8_t>
+make_remote_non_struct_type_meta(TypeId type_id, const std::string &type_name) {
+  TypeMeta meta =
+      TypeMeta::from_fields(static_cast<uint32_t>(type_id), "example",
+                            type_name, true, 0, std::vector<FieldInfo>{});
+  auto bytes = meta.to_bytes();
+  EXPECT_TRUE(bytes.ok()) << "TypeMeta serialization failed: "
+                          << bytes.error().to_string();
+  return bytes.value();
+}
+
+static Result<const TypeInfo *, Error>
+append_and_read_type_meta(ReadContext &ctx, const std::vector<uint8_t> &bytes) {
+  Buffer buffer;
+  buffer.write_var_uint32(0);
+  buffer.write_bytes(bytes.data(), static_cast<uint32_t>(bytes.size()));
+  ctx.reset();
+  ctx.attach(buffer);
+  auto result = ctx.read_type_meta();
+  ctx.detach();
+  return result;
+}
+
+TEST(SerializationTest, RemoteSchemaLimitRejectsExtraVersions) {
+  Config config;
+  config.compatible = true;
+  config.max_schema_versions_per_type = 1;
+  ReadContext ctx(config, std::make_unique<TypeResolver>());
+
+  auto first = append_and_read_type_meta(
+      ctx, make_remote_type_meta("Unknown", "first_value"));
+  ASSERT_TRUE(first.ok()) << first.error().to_string();
+
+  auto second = append_and_read_type_meta(
+      ctx, make_remote_type_meta("Unknown", "second_value"));
+  EXPECT_FALSE(second.ok());
+  ASSERT_FALSE(second.ok());
+  EXPECT_EQ(second.error().code(), ErrorCode::InvalidData);
+}
+
+TEST(SerializationTest, RemoteNonStructTypeMetaUsesSchemaLimit) {
+  Config config;
+  config.compatible = true;
+  config.max_schema_versions_per_type = 1;
+  ReadContext ctx(config, std::make_unique<TypeResolver>());
+
+  auto first = append_and_read_type_meta(
+      ctx, make_remote_non_struct_type_meta(TypeId::NAMED_ENUM, "RemoteEnum"));
+  ASSERT_TRUE(first.ok()) << first.error().to_string();
+
+  auto second = append_and_read_type_meta(
+      ctx, make_remote_non_struct_type_meta(TypeId::NAMED_EXT, "RemoteEnum"));
+  EXPECT_FALSE(second.ok());
+  ASSERT_FALSE(second.ok());
+  EXPECT_EQ(second.error().code(), ErrorCode::InvalidData);
+}
+
+TEST(SerializationTest, ExactLocalNonStructTypeMetaBypassesLimit) {
+  auto fory = Fory::builder()
+                  .xlang(true)
+                  .compatible(true)
+                  .max_schema_versions_per_type(1)
+                  .build();
+  ASSERT_TRUE(
+      fory.register_enum<SignedScopedStatus>("example", "SharedEnum").ok());
+  auto local_type_info =
+      fory.type_resolver().get_type_info<SignedScopedStatus>();
+  ASSERT_TRUE(local_type_info.ok()) << local_type_info.error().to_string();
+  std::vector<uint8_t> exact = local_type_info.value()->type_def;
+  ReadContext ctx(fory.config(), fory.type_resolver().clone());
+
+  auto first = append_and_read_type_meta(ctx, exact);
+  ASSERT_TRUE(first.ok()) << first.error().to_string();
+  auto second = append_and_read_type_meta(
+      ctx, make_remote_non_struct_type_meta(TypeId::NAMED_EXT, "SharedEnum"));
+  ASSERT_TRUE(second.ok()) << second.error().to_string();
+}
+
+TEST(SerializationTest, ExactLocalNamedUnionTypeMetaBypassesLimit) {
+  using LocalUnion = std::variant<int32_t, std::string>;
+  auto fory = Fory::builder()
+                  .xlang(true)
+                  .compatible(true)
+                  .max_schema_versions_per_type(1)
+                  .build();
+  ASSERT_TRUE(fory.register_union<LocalUnion>("example", "SharedUnion").ok());
+  auto local_type_info = fory.type_resolver().get_type_info<LocalUnion>();
+  ASSERT_TRUE(local_type_info.ok()) << local_type_info.error().to_string();
+  std::vector<uint8_t> exact = local_type_info.value()->type_def;
+  ASSERT_FALSE(exact.empty());
+  ReadContext ctx(fory.config(), fory.type_resolver().clone());
+
+  auto first = append_and_read_type_meta(ctx, exact);
+  ASSERT_TRUE(first.ok()) << first.error().to_string();
+  auto second = append_and_read_type_meta(
+      ctx, make_remote_non_struct_type_meta(TypeId::NAMED_EXT, "SharedUnion"));
+  ASSERT_TRUE(second.ok()) << second.error().to_string();
+}
+
+TEST(SerializationTest, RemoteSchemaLimitKeepsUnknownTypesSeparate) {
+  Config config;
+  config.compatible = true;
+  config.max_schema_versions_per_type = 1;
+  ReadContext ctx(config, std::make_unique<TypeResolver>());
+
+  auto first = append_and_read_type_meta(
+      ctx, make_remote_type_meta("UnknownA", "value"));
+  ASSERT_TRUE(first.ok()) << first.error().to_string();
+  auto second = append_and_read_type_meta(
+      ctx, make_remote_type_meta("UnknownB", "value"));
+  EXPECT_TRUE(second.ok()) << second.error().to_string();
+}
+
+TEST(SerializationTest, IdEnumDoesNotUseTypeMetaLimits) {
+  auto fory = Fory::builder()
+                  .xlang(true)
+                  .compatible(true)
+                  .max_type_meta_bytes(1)
+                  .max_schema_versions_per_type(1)
+                  .build();
+  ASSERT_TRUE(fory.register_enum<SignedScopedStatus>(101).ok());
+
+  auto bytes = fory.serialize(SignedScopedStatus::LARGE);
+  ASSERT_TRUE(bytes.ok()) << bytes.error().to_string();
+  std::vector<uint8_t> payload = std::move(bytes).value();
+  auto decoded =
+      fory.deserialize<SignedScopedStatus>(payload.data(), payload.size());
+  ASSERT_TRUE(decoded.ok()) << decoded.error().to_string();
+  EXPECT_EQ(decoded.value(), SignedScopedStatus::LARGE);
+}
+
+TEST(SerializationTest, IdExtDoesNotUseTypeMetaLimits) {
+  auto fory = Fory::builder()
+                  .xlang(true)
+                  .compatible(true)
+                  .max_type_meta_bytes(1)
+                  .max_schema_versions_per_type(1)
+                  .build();
+  ASSERT_TRUE(fory.register_extension_type<IdLimitExt>(102).ok());
+
+  auto bytes = fory.serialize(IdLimitExt{42});
+  ASSERT_TRUE(bytes.ok()) << bytes.error().to_string();
+  std::vector<uint8_t> payload = std::move(bytes).value();
+  auto decoded = fory.deserialize<IdLimitExt>(payload.data(), payload.size());
+  ASSERT_TRUE(decoded.ok()) << decoded.error().to_string();
+  EXPECT_EQ(decoded.value(), IdLimitExt{42});
+}
+
+TEST(SerializationTest, LocalTypeMetaFinalizationIgnoresReceiveBodyLimit) {
+  auto fory = Fory::builder()
+                  .xlang(true)
+                  .compatible(true)
+                  .max_type_meta_bytes(1)
+                  .build();
+  ASSERT_TRUE(fory.register_struct<SimpleStruct>(103).ok());
+
+  auto bytes = fory.serialize(SimpleStruct{1, 2});
+  ASSERT_TRUE(bytes.ok()) << bytes.error().to_string();
+}
+
 TEST(SerializationTest, TypeMetaRejectsOverConsumedDeclaredSize) {
   TypeMeta meta =
       TypeMeta::from_fields(static_cast<uint32_t>(TypeId::STRUCT), "", "S",
@@ -1021,6 +1206,52 @@ TEST(SerializationTest, TypeMetaHeaderUses52BitMetadataHash) {
             parsed.value()->get_hash());
 }
 
+TEST(SerializationTest, TypeMetaRejectsMaxTypeFields) {
+  std::vector<FieldInfo> fields;
+  fields.emplace_back(
+      "first", FieldType(static_cast<uint32_t>(TypeId::VARINT32), false));
+  fields.emplace_back(
+      "second", FieldType(static_cast<uint32_t>(TypeId::VARINT32), false));
+  TypeMeta meta = TypeMeta::from_fields(static_cast<uint32_t>(TypeId::STRUCT),
+                                        "", "S", false, 1, std::move(fields));
+  auto bytes_result = meta.to_bytes();
+  ASSERT_TRUE(bytes_result.ok())
+      << "TypeMeta serialization failed: " << bytes_result.error().to_string();
+  std::vector<uint8_t> bytes = bytes_result.value();
+  Buffer buffer(bytes);
+  Error error;
+  int64_t header = 0;
+  buffer.read_bytes(&header, sizeof(header), error);
+  ASSERT_TRUE(error.ok()) << error.to_string();
+
+  auto parsed = TypeMeta::from_bytes_with_header(buffer, header, 1, 4096);
+  ASSERT_FALSE(parsed.ok());
+  EXPECT_NE(parsed.error().to_string().find("max_type_fields"),
+            std::string::npos);
+}
+
+TEST(SerializationTest, TypeMetaRejectsMaxTypeMetaBytes) {
+  std::vector<FieldInfo> fields;
+  fields.emplace_back(
+      "value", FieldType(static_cast<uint32_t>(TypeId::VARINT32), false));
+  TypeMeta meta = TypeMeta::from_fields(static_cast<uint32_t>(TypeId::STRUCT),
+                                        "", "S", false, 1, std::move(fields));
+  auto bytes_result = meta.to_bytes();
+  ASSERT_TRUE(bytes_result.ok())
+      << "TypeMeta serialization failed: " << bytes_result.error().to_string();
+  std::vector<uint8_t> bytes = bytes_result.value();
+  Buffer buffer(bytes);
+  Error error;
+  int64_t header = 0;
+  buffer.read_bytes(&header, sizeof(header), error);
+  ASSERT_TRUE(error.ok()) << error.to_string();
+
+  auto parsed = TypeMeta::from_bytes_with_header(buffer, header, 512, 1);
+  ASSERT_FALSE(parsed.ok());
+  EXPECT_NE(parsed.error().to_string().find("max_type_meta_bytes"),
+            std::string::npos);
+}
+
 TEST(SerializationTest, TypeMetaRejectsBodyOnlyHeaderHash) {
   TypeMeta meta =
       TypeMeta::from_fields(static_cast<uint32_t>(TypeId::STRUCT), "", "S",
diff --git a/cpp/fory/serialization/type_resolver.cc b/cpp/fory/serialization/type_resolver.cc
index 45db2d654..f00aba3bb 100644
--- a/cpp/fory/serialization/type_resolver.cc
+++ b/cpp/fory/serialization/type_resolver.cc
@@ -450,6 +450,30 @@ read_type_meta_size(Buffer &buffer, uint64_t header, size_t *header_size) {
   return static_cast<uint32_t>(meta_size);
 }
 
+inline Result<void, Error>
+check_type_meta_body_size(uint32_t meta_size, uint32_t max_type_meta_bytes) {
+  if (FORY_PREDICT_FALSE(meta_size > max_type_meta_bytes)) {
+    return Unexpected(Error::invalid_data(
+        "Type metadata body size " + std::to_string(meta_size) +
+        " exceeds max_type_meta_bytes " + std::to_string(max_type_meta_bytes) +
+        ". The data may be malicious. If the data is not malicious, please "
+        "increase max_type_meta_bytes."));
+  }
+  return Result<void, Error>();
+}
+
+inline Result<void, Error> check_type_meta_fields(size_t num_fields,
+                                                  uint32_t max_type_fields) {
+  if (FORY_PREDICT_FALSE(num_fields > max_type_fields)) {
+    return Unexpected(Error::invalid_data(
+        "Type metadata field count " + std::to_string(num_fields) +
+        " exceeds max_type_fields " + std::to_string(max_type_fields) +
+        ". The data may be malicious. If the data is not malicious, please "
+        "increase max_type_fields."));
+  }
+  return Result<void, Error>();
+}
+
 inline Result<void, Error> validate_type_meta_hash(Buffer &buffer,
                                                    uint32_t body_start,
                                                    uint32_t meta_size,
@@ -620,7 +644,8 @@ Result<std::vector<uint8_t>, Error> TypeMeta::to_bytes() const {
 }
 
 Result<std::unique_ptr<TypeMeta>, Error>
-TypeMeta::from_bytes(Buffer &buffer, const TypeMeta *local_type_info) {
+TypeMeta::from_bytes(Buffer &buffer, const TypeMeta *local_type_info,
+                     uint32_t max_type_fields, uint32_t max_type_meta_bytes) {
   size_t start_pos = buffer.reader_index();
 
   // Read global binary header
@@ -635,8 +660,12 @@ TypeMeta::from_bytes(Buffer &buffer, const TypeMeta *local_type_info) {
   uint64_t header_bits = static_cast<uint64_t>(header);
   FORY_RETURN_IF_ERROR(validate_type_meta_header(header_bits));
   FORY_TRY(meta_size, read_type_meta_size(buffer, header_bits, &header_size));
+  FORY_RETURN_IF_ERROR(
+      check_type_meta_body_size(meta_size, max_type_meta_bytes));
   int64_t meta_hash = static_cast<int64_t>(header_bits >> TYPE_META_HASH_SHIFT);
   uint32_t body_start = static_cast<uint32_t>(start_pos + header_size);
+  // The size cap is not byte-availability proof. Ensure the declared body is
+  // readable before any parsing, copying, or cached metadata publication.
   if (FORY_PREDICT_FALSE(!buffer.ensure_readable(meta_size, error))) {
     return Unexpected(std::move(error));
   }
@@ -671,6 +700,7 @@ TypeMeta::from_bytes(Buffer &buffer, const TypeMeta *local_type_info) {
       }
       num_fields += extra;
     }
+    FORY_RETURN_IF_ERROR(check_type_meta_fields(num_fields, max_type_fields));
   } else {
     if (FORY_PREDICT_FALSE((meta_header & NON_STRUCT_RESERVED_BITS_MASK) !=
                            0)) {
@@ -752,14 +782,20 @@ TypeMeta::from_bytes(Buffer &buffer, const TypeMeta *local_type_info) {
 }
 
 Result<std::unique_ptr<TypeMeta>, Error>
-TypeMeta::from_bytes_with_header(Buffer &buffer, int64_t header) {
+TypeMeta::from_bytes_with_header(Buffer &buffer, int64_t header,
+                                 uint32_t max_type_fields,
+                                 uint32_t max_type_meta_bytes) {
   uint64_t header_bits = static_cast<uint64_t>(header);
   FORY_RETURN_IF_ERROR(validate_type_meta_header(header_bits));
   FORY_TRY(meta_size, read_type_meta_size(buffer, header_bits, nullptr));
+  FORY_RETURN_IF_ERROR(
+      check_type_meta_body_size(meta_size, max_type_meta_bytes));
   int64_t meta_hash = static_cast<int64_t>(header_bits >> TYPE_META_HASH_SHIFT);
 
   uint32_t start_pos = buffer.reader_index();
   Error error;
+  // The size cap is not byte-availability proof. Ensure the declared body is
+  // readable before any parsing, copying, or cached metadata publication.
   if (FORY_PREDICT_FALSE(!buffer.ensure_readable(meta_size, error))) {
     return Unexpected(std::move(error));
   }
@@ -795,6 +831,7 @@ TypeMeta::from_bytes_with_header(Buffer &buffer, int64_t header) {
       }
       num_fields += extra;
     }
+    FORY_RETURN_IF_ERROR(check_type_meta_fields(num_fields, max_type_fields));
   } else {
     if (FORY_PREDICT_FALSE((meta_header & NON_STRUCT_RESERVED_BITS_MASK) !=
                            0)) {
@@ -871,6 +908,8 @@ TypeMeta::from_bytes_with_header(Buffer &buffer, int64_t header) {
 
 Result<void, Error> TypeMeta::skip_bytes_for_validated_header(Buffer &buffer,
                                                               int64_t header) {
+  // Header-cache hits intentionally skip opaque metadata. This path must not
+  // allocate or materialize the body from the attacker-declared size.
   Error error;
   uint64_t meta_size = static_cast<uint64_t>(header) & META_SIZE_MASK;
   if (meta_size == META_SIZE_MASK) {
@@ -1775,7 +1814,13 @@ TypeResolver::build_final_type_resolver() {
     Buffer buffer(partial_ptr->type_def.data(),
                   static_cast<uint32_t>(partial_ptr->type_def.size()), false);
     buffer.writer_index(static_cast<uint32_t>(partial_ptr->type_def.size()));
-    FORY_TRY(parsed_meta, TypeMeta::from_bytes(buffer, nullptr));
+    // This metadata was just generated from local registration state. Remote
+    // receive limits are enforced only on remote metadata parse/cache-miss
+    // paths, so large trusted local schemas do not fail during finalization.
+    FORY_TRY(parsed_meta,
+             TypeMeta::from_bytes(buffer, nullptr,
+                                  std::numeric_limits<uint32_t>::max(),
+                                  std::numeric_limits<uint32_t>::max()));
     partial_ptr->type_meta = std::move(parsed_meta);
   }
 
diff --git a/cpp/fory/serialization/type_resolver.h b/cpp/fory/serialization/type_resolver.h
index dcf358b13..1b8eea007 100644
--- a/cpp/fory/serialization/type_resolver.h
+++ b/cpp/fory/serialization/type_resolver.h
@@ -220,8 +220,6 @@ public:
 // TypeMeta - Complete type metadata (for schema evolution)
 // ============================================================================
 
-constexpr size_t MAX_PARSED_NUM_TYPE_DEFS = 8192;
-
 /// Type metadata containing all field information
 /// Used for schema evolution to compare remote and local type schemas
 class TypeMeta {
@@ -252,13 +250,17 @@ public:
   /// @param buffer Source buffer
   /// @param local_type_info Local type information (for field ID assignment)
   static Result<std::unique_ptr<TypeMeta>, Error>
-  from_bytes(Buffer &buffer, const TypeMeta *local_type_info);
+  from_bytes(Buffer &buffer, const TypeMeta *local_type_info,
+             uint32_t max_type_fields = 512,
+             uint32_t max_type_meta_bytes = 4096);
 
   /// Read type meta from buffer with pre-read header
   /// @param buffer Source buffer (positioned after header)
   /// @param header Pre-read 8-byte header
   static Result<std::unique_ptr<TypeMeta>, Error>
-  from_bytes_with_header(Buffer &buffer, int64_t header);
+  from_bytes_with_header(Buffer &buffer, int64_t header,
+                         uint32_t max_type_fields = 512,
+                         uint32_t max_type_meta_bytes = 4096);
 
   /// skip type meta in buffer without parsing
   static Result<void, Error> skip_bytes_for_validated_header(Buffer &buffer,
@@ -1964,6 +1966,14 @@ TypeResolver::build_enum_type_info(uint32_t type_id, uint32_t user_type_id,
   FORY_TRY(enc_tn, encode_meta_string(entry->type_name, false));
   entry->encoded_type_name = std::move(enc_tn);
 
+  if (register_by_name) {
+    TypeMeta meta = TypeMeta::from_fields(
+        type_id, entry->namespace_name, entry->type_name, register_by_name,
+        entry->user_type_id, std::vector<FieldInfo>{});
+    FORY_TRY(type_def, meta.to_bytes());
+    entry->type_def = std::move(type_def);
+  }
+
   return entry;
 }
 
@@ -2030,6 +2040,14 @@ TypeResolver::build_union_type_info(uint32_t type_id, uint32_t user_type_id,
   FORY_TRY(enc_tn, encode_meta_string(entry->type_name, false));
   entry->encoded_type_name = std::move(enc_tn);
 
+  if (register_by_name) {
+    TypeMeta meta = TypeMeta::from_fields(
+        type_id, entry->namespace_name, entry->type_name, register_by_name,
+        entry->user_type_id, std::vector<FieldInfo>{});
+    FORY_TRY(type_def, meta.to_bytes());
+    entry->type_def = std::move(type_def);
+  }
+
   return entry;
 }
 
diff --git a/csharp/src/Fory.Generator/ForyModelGenerator.cs b/csharp/src/Fory.Generator/ForyModelGenerator.cs
index fd1bfefba..8e051da47 100644
--- a/csharp/src/Fory.Generator/ForyModelGenerator.cs
+++ b/csharp/src/Fory.Generator/ForyModelGenerator.cs
@@ -204,18 +204,18 @@ public sealed class ForyModelGenerator : IIncrementalGenerator
         sb.AppendLine("{");
         sb.AppendLine("    private static readonly object __ForyTypeMetaCacheLock = new();");
         sb.AppendLine("    private static ulong __ForyTypeMetaResolverVersion;");
-        sb.AppendLine("    private static ulong __ForyTypeMetaHeaderHashNoTrackRef;");
-        sb.AppendLine("    private static ulong __ForyTypeMetaHeaderHashTrackRef;");
-        sb.AppendLine("    private static global::Apache.Fory.TypeMeta? __ForyLastTypeMetaNoTrackRef;");
-        sb.AppendLine("    private static bool __ForyLastTypeMetaMatchedNoTrackRef;");
-        sb.AppendLine("    private static global::Apache.Fory.TypeMeta? __ForyLastTypeMetaTrackRef;");
-        sb.AppendLine("    private static bool __ForyLastTypeMetaMatchedTrackRef;");
+        sb.AppendLine("    private static ulong __ForyNoRefTypeMetaHash;");
+        sb.AppendLine("    private static ulong __ForyRefTypeMetaHash;");
+        sb.AppendLine("    private static global::Apache.Fory.TypeMeta? __ForyNoRefMeta;");
+        sb.AppendLine("    private static bool __ForyNoRefMetaMatches;");
+        sb.AppendLine("    private static global::Apache.Fory.TypeMeta? __ForyRefMeta;");
+        sb.AppendLine("    private static bool __ForyRefMetaMatches;");
         sb.AppendLine(
             $"    private const bool __ForyAllFieldsBuiltIn = {BoolLiteral(model.SortedMembers.All(m => m.DynamicAnyKind == DynamicAnyKind.None && m.Classification.IsBuiltIn))};");
         sb.AppendLine(
-            "    private static global::System.Collections.Generic.IReadOnlyList<global::Apache.Fory.TypeMetaFieldInfo>? __ForyTypeMetaFieldsNoTrackRef;");
+            "    private static global::System.Collections.Generic.IReadOnlyList<global::Apache.Fory.TypeMetaFieldInfo>? __ForyNoRefTypeMetaFields;");
         sb.AppendLine(
-            "    private static global::System.Collections.Generic.IReadOnlyList<global::Apache.Fory.TypeMetaFieldInfo>? __ForyTypeMetaFieldsTrackRef;");
+            "    private static global::System.Collections.Generic.IReadOnlyList<global::Apache.Fory.TypeMetaFieldInfo>? __ForyRefTypeMetaFields;");
 
         if (model.SortedMembers.Length > 0)
         {
@@ -319,13 +319,13 @@ public sealed class ForyModelGenerator : IIncrementalGenerator
         sb.AppendLine(
             $"            global::Apache.Fory.TypeInfo typeInfo = typeResolver.GetTypeInfo<{model.TypeName}>();");
         sb.AppendLine(
-            "            __ForyTypeMetaHeaderHashNoTrackRef = typeInfo.GetTypeMetaHeaderHash(false);");
+            "            __ForyNoRefTypeMetaHash = typeInfo.GetTypeMetaHeaderHash(false);");
         sb.AppendLine(
-            "            __ForyTypeMetaHeaderHashTrackRef = typeInfo.GetTypeMetaHeaderHash(true);");
-        sb.AppendLine("            __ForyLastTypeMetaNoTrackRef = null;");
-        sb.AppendLine("            __ForyLastTypeMetaMatchedNoTrackRef = false;");
-        sb.AppendLine("            __ForyLastTypeMetaTrackRef = null;");
-        sb.AppendLine("            __ForyLastTypeMetaMatchedTrackRef = false;");
+            "            __ForyRefTypeMetaHash = typeInfo.GetTypeMetaHeaderHash(true);");
+        sb.AppendLine("            __ForyNoRefMeta = null;");
+        sb.AppendLine("            __ForyNoRefMetaMatches = false;");
+        sb.AppendLine("            __ForyRefMeta = null;");
+        sb.AppendLine("            __ForyRefMetaMatches = false;");
         sb.AppendLine("            __ForyTypeMetaResolverVersion = resolverVersion;");
         sb.AppendLine("        }");
         sb.AppendLine("    }");
@@ -336,50 +336,50 @@ public sealed class ForyModelGenerator : IIncrementalGenerator
         sb.AppendLine("        if (trackRef)");
         sb.AppendLine("        {");
         sb.AppendLine(
-            "            if (global::System.Object.ReferenceEquals(__ForyLastTypeMetaTrackRef, typeMeta))");
+            "            if (global::System.Object.ReferenceEquals(__ForyRefMeta, typeMeta))");
         sb.AppendLine("            {");
-        sb.AppendLine("                return __ForyLastTypeMetaMatchedTrackRef;");
+        sb.AppendLine("                return __ForyRefMetaMatches;");
         sb.AppendLine("            }");
         sb.AppendLine();
         sb.AppendLine("            __ForyEnsureTypeMetaCache(typeResolver);");
         sb.AppendLine();
         sb.AppendLine("            bool matched = false;");
-        sb.AppendLine("            if (typeMeta.HeaderHash == __ForyTypeMetaHeaderHashTrackRef)");
+        sb.AppendLine("            if (typeMeta.HeaderHash == __ForyRefTypeMetaHash)");
         sb.AppendLine("            {");
         sb.AppendLine("                matched = __ForyMatchesTypeMeta(typeMeta, true);");
         sb.AppendLine("            }");
         sb.AppendLine();
-        sb.AppendLine("            __ForyLastTypeMetaTrackRef = typeMeta;");
-        sb.AppendLine("            __ForyLastTypeMetaMatchedTrackRef = matched;");
+        sb.AppendLine("            __ForyRefMeta = typeMeta;");
+        sb.AppendLine("            __ForyRefMetaMatches = matched;");
         sb.AppendLine("            return matched;");
         sb.AppendLine("        }");
         sb.AppendLine();
         sb.AppendLine(
-            "        if (global::System.Object.ReferenceEquals(__ForyLastTypeMetaNoTrackRef, typeMeta))");
+            "        if (global::System.Object.ReferenceEquals(__ForyNoRefMeta, typeMeta))");
         sb.AppendLine("        {");
-        sb.AppendLine("            return __ForyLastTypeMetaMatchedNoTrackRef;");
+        sb.AppendLine("            return __ForyNoRefMetaMatches;");
         sb.AppendLine("        }");
         sb.AppendLine();
         sb.AppendLine("        __ForyEnsureTypeMetaCache(typeResolver);");
         sb.AppendLine();
         sb.AppendLine("        bool noTrackMatched = false;");
-        sb.AppendLine("        if (typeMeta.HeaderHash == __ForyTypeMetaHeaderHashNoTrackRef)");
+        sb.AppendLine("        if (typeMeta.HeaderHash == __ForyNoRefTypeMetaHash)");
         sb.AppendLine("        {");
         sb.AppendLine("            noTrackMatched = __ForyMatchesTypeMeta(typeMeta, false);");
         sb.AppendLine("        }");
         sb.AppendLine();
-        sb.AppendLine("        __ForyLastTypeMetaNoTrackRef = typeMeta;");
-        sb.AppendLine("        __ForyLastTypeMetaMatchedNoTrackRef = noTrackMatched;");
+        sb.AppendLine("        __ForyNoRefMeta = typeMeta;");
+        sb.AppendLine("        __ForyNoRefMetaMatches = noTrackMatched;");
         sb.AppendLine("        return noTrackMatched;");
         sb.AppendLine("    }");
         sb.AppendLine();
-        sb.AppendLine("    private static uint? __ForySchemaHashNoTrackRef;");
+        sb.AppendLine("    private static uint? __ForyNoRefSchemaHash;");
         sb.AppendLine();
         sb.AppendLine("    private static uint __ForySchemaHash(bool trackRef, global::Apache.Fory.TypeResolver typeResolver)");
         sb.AppendLine("    {");
-        sb.AppendLine("        if (!trackRef && __ForySchemaHashNoTrackRef.HasValue)");
+        sb.AppendLine("        if (!trackRef && __ForyNoRefSchemaHash.HasValue)");
         sb.AppendLine("        {");
-        sb.AppendLine("            return __ForySchemaHashNoTrackRef.Value;");
+        sb.AppendLine("            return __ForyNoRefSchemaHash.Value;");
         sb.AppendLine("        }");
         sb.AppendLine();
         sb.Append("        uint value = global::Apache.Fory.SchemaHash.StructHash32(");
@@ -387,7 +387,7 @@ public sealed class ForyModelGenerator : IIncrementalGenerator
         sb.AppendLine(");");
         sb.AppendLine("        if (!trackRef)");
         sb.AppendLine("        {");
-        sb.AppendLine("            __ForySchemaHashNoTrackRef = value;");
+        sb.AppendLine("            __ForyNoRefSchemaHash = value;");
         sb.AppendLine("        }");
         sb.AppendLine();
         sb.AppendLine("        return value;");
@@ -408,11 +408,11 @@ public sealed class ForyModelGenerator : IIncrementalGenerator
         sb.AppendLine("        if (trackRef)");
         sb.AppendLine("        {");
         sb.AppendLine(
-            "            return __ForyTypeMetaFieldsTrackRef ??= __ForyBuildTypeMetaFields(true);");
+            "            return __ForyRefTypeMetaFields ??= __ForyBuildTypeMetaFields(true);");
         sb.AppendLine("        }");
         sb.AppendLine();
         sb.AppendLine(
-            "        return __ForyTypeMetaFieldsNoTrackRef ??= __ForyBuildTypeMetaFields(false);");
+            "        return __ForyNoRefTypeMetaFields ??= __ForyBuildTypeMetaFields(false);");
         sb.AppendLine("    }");
         sb.AppendLine();
         sb.AppendLine(
diff --git a/csharp/src/Fory/Config.cs b/csharp/src/Fory/Config.cs
index 2d8d94896..438039d2c 100644
--- a/csharp/src/Fory/Config.cs
+++ b/csharp/src/Fory/Config.cs
@@ -27,12 +27,41 @@ public sealed class Config
         bool trackRef,
         bool compatible,
         bool checkStructVersion,
-        int maxDepth)
+        int maxDepth,
+        int maxTypeFields,
+        int maxTypeMetaBytes,
+        int maxSchemaVersionsPerType,
+        int maxAverageSchemaVersionsPerType)
     {
+        if (maxDepth <= 0)
+        {
+            throw new ArgumentOutOfRangeException(nameof(maxDepth), "MaxDepth must be greater than 0.");
+        }
+        if (maxTypeFields <= 0)
+        {
+            throw new ArgumentOutOfRangeException(nameof(maxTypeFields), "MaxTypeFields must be greater than 0.");
+        }
+        if (maxTypeMetaBytes <= 0)
+        {
+            throw new ArgumentOutOfRangeException(nameof(maxTypeMetaBytes), "MaxTypeMetaBytes must be greater than 0.");
+        }
+        if (maxSchemaVersionsPerType <= 0)
+        {
+            throw new ArgumentOutOfRangeException(nameof(maxSchemaVersionsPerType), "MaxSchemaVersionsPerType must be greater than 0.");
+        }
+        if (maxAverageSchemaVersionsPerType <= 0)
+        {
+            throw new ArgumentOutOfRangeException(nameof(maxAverageSchemaVersionsPerType), "MaxAverageSchemaVersionsPerType must be greater than 0.");
+        }
+
         TrackRef = trackRef;
         Compatible = compatible;
         CheckStructVersion = checkStructVersion;
         MaxDepth = maxDepth;
+        MaxTypeFields = maxTypeFields;
+        MaxTypeMetaBytes = maxTypeMetaBytes;
+        MaxSchemaVersionsPerType = maxSchemaVersionsPerType;
+        MaxAverageSchemaVersionsPerType = maxAverageSchemaVersionsPerType;
     }
 
     /// <summary>
@@ -54,6 +83,26 @@ public sealed class Config
     /// Gets the maximum allowed nesting depth for dynamic object payload reads.
     /// </summary>
     public int MaxDepth { get; }
+
+    /// <summary>
+    /// Gets the maximum accepted field count in one received struct TypeMeta.
+    /// </summary>
+    public int MaxTypeFields { get; }
+
+    /// <summary>
+    /// Gets the maximum accepted body size in one received TypeMeta.
+    /// </summary>
+    public int MaxTypeMetaBytes { get; }
+
+    /// <summary>
+    /// Gets the maximum accepted remote metadata versions for one logical type.
+    /// </summary>
+    public int MaxSchemaVersionsPerType { get; }
+
+    /// <summary>
+    /// Gets the average remote metadata version limit across accepted remote types.
+    /// </summary>
+    public int MaxAverageSchemaVersionsPerType { get; }
 }
 
 /// <summary>
@@ -65,6 +114,10 @@ public sealed class ForyBuilder
     private bool? _compatible;
     private bool _checkStructVersion;
     private int _maxDepth = 20;
+    private int _maxTypeFields = 512;
+    private int _maxTypeMetaBytes = 4096;
+    private int _maxSchemaVersionsPerType = 10;
+    private int _maxAverageSchemaVersionsPerType = 3;
 
     /// <summary>
     /// Enables or disables reference tracking for shared and circular object graphs.
@@ -116,6 +169,62 @@ public sealed class ForyBuilder
         return this;
     }
 
+    /// <summary>
+    /// Sets the maximum accepted field count in one received struct TypeMeta.
+    /// </summary>
+    public ForyBuilder MaxTypeFields(int value)
+    {
+        if (value <= 0)
+        {
+            throw new ArgumentOutOfRangeException(nameof(value), "MaxTypeFields must be greater than 0.");
+        }
+
+        _maxTypeFields = value;
+        return this;
+    }
+
+    /// <summary>
+    /// Sets the maximum accepted body size in one received TypeMeta.
+    /// </summary>
+    public ForyBuilder MaxTypeMetaBytes(int value)
+    {
+        if (value <= 0)
+        {
+            throw new ArgumentOutOfRangeException(nameof(value), "MaxTypeMetaBytes must be greater than 0.");
+        }
+
+        _maxTypeMetaBytes = value;
+        return this;
+    }
+
+    /// <summary>
+    /// Sets the maximum accepted remote metadata versions for one logical type.
+    /// </summary>
+    public ForyBuilder MaxSchemaVersionsPerType(int value)
+    {
+        if (value <= 0)
+        {
+            throw new ArgumentOutOfRangeException(nameof(value), "MaxSchemaVersionsPerType must be greater than 0.");
+        }
+
+        _maxSchemaVersionsPerType = value;
+        return this;
+    }
+
+    /// <summary>
+    /// Sets the average remote metadata version limit across accepted remote types.
+    /// </summary>
+    public ForyBuilder MaxAverageSchemaVersionsPerType(int value)
+    {
+        if (value <= 0)
+        {
+            throw new ArgumentOutOfRangeException(nameof(value), "MaxAverageSchemaVersionsPerType must be greater than 0.");
+        }
+
+        _maxAverageSchemaVersionsPerType = value;
+        return this;
+    }
+
     private Config BuildConfig()
     {
         bool compatible = _compatible ?? true;
@@ -125,7 +234,11 @@ public sealed class ForyBuilder
             trackRef: _trackRef,
             compatible: compatible,
             checkStructVersion: compatible ? false : _checkStructVersion,
-            maxDepth: _maxDepth);
+            maxDepth: _maxDepth,
+            maxTypeFields: _maxTypeFields,
+            maxTypeMetaBytes: _maxTypeMetaBytes,
+            maxSchemaVersionsPerType: _maxSchemaVersionsPerType,
+            maxAverageSchemaVersionsPerType: _maxAverageSchemaVersionsPerType);
     }
 
     /// <summary>
diff --git a/csharp/src/Fory/Fory.cs b/csharp/src/Fory/Fory.cs
index f193d4b92..9bbafd177 100644
--- a/csharp/src/Fory/Fory.cs
+++ b/csharp/src/Fory/Fory.cs
@@ -44,10 +44,7 @@ public sealed class Fory
         _readContext = new ReadContext(
             new ByteReader(Array.Empty<byte>()),
             _typeResolver,
-            Config.TrackRef,
-            Config.Compatible,
-            Config.CheckStructVersion,
-            Config.MaxDepth);
+            Config);
     }
 
     /// <summary>
diff --git a/csharp/src/Fory/ReadContext.cs b/csharp/src/Fory/ReadContext.cs
index 2192bb55c..f83ac0e99 100644
--- a/csharp/src/Fory/ReadContext.cs
+++ b/csharp/src/Fory/ReadContext.cs
@@ -19,15 +19,12 @@ namespace Apache.Fory;
 
 public sealed class ReadContext
 {
-    private const int MaxParsedTypeMetaEntries = 8192;
+    private const int MinRemoteTypeMetaLimit = 8192;
 
-    private readonly ReusableArray<TypeMeta> _readTypeMetas = new();
-    private readonly Dictionary<ulong, TypeMeta> _cachedTypeMetasByHeader = [];
-    private TypeMeta? _firstReadTypeMeta;
-    private bool _hasFirstReadTypeMeta;
-    private ulong _lastMetaHeader;
-    private TypeMeta? _lastTypeMeta;
-    private bool _hasLastMetaHeader;
+    private readonly ReusableArray<TypeMeta> _typeMetaRefs = new();
+    private readonly UInt64Map<TypeMeta> _typeMetasByHeader = new();
+    private TypeMeta? _firstTypeMetaRef;
+    private bool _hasFirstTypeMetaRef;
 
     private readonly List<MetaString> _readMetaStrings = [];
 
@@ -40,27 +37,25 @@ public sealed class ReadContext
     internal Type? _cachedTypeMetaType;
     internal TypeMeta? _cachedTypeMeta;
     internal int _currentDynamicReadDepth;
+    private readonly Dictionary<object, int> _remoteSchemaVersionsByType = [];
+    private readonly Config _config;
+    private int _totalAcceptedSchemaVersions;
 
     public ReadContext(
         ByteReader reader,
         TypeResolver typeResolver,
-        bool trackRef,
-        bool compatible = false,
-        bool checkStructVersion = false,
-        int maxDynamicReadDepth = 20)
+        Config config)
     {
-        if (maxDynamicReadDepth <= 0)
-        {
-            throw new ArgumentOutOfRangeException(nameof(maxDynamicReadDepth), "MaxDepth must be greater than 0.");
-        }
+        ArgumentNullException.ThrowIfNull(config);
 
         Reader = reader;
         TypeResolver = typeResolver;
-        TrackRef = trackRef;
-        Compatible = compatible;
-        CheckStructVersion = checkStructVersion;
+        TrackRef = config.TrackRef;
+        Compatible = config.Compatible;
+        CheckStructVersion = config.CheckStructVersion;
         RefReader = new RefReader();
-        _maxDynamicReadDepth = maxDynamicReadDepth;
+        _maxDynamicReadDepth = config.MaxDepth;
+        _config = config;
     }
 
     public ByteReader Reader { get; private set; }
@@ -81,7 +76,7 @@ public sealed class ReadContext
         Reset();
     }
 
-    internal TypeMeta? GetReadTypeMeta(int index)
+    internal TypeMeta? GetTypeMetaRef(int index)
     {
         if (index < 0)
         {
@@ -90,13 +85,13 @@ public sealed class ReadContext
 
         if (index == 0)
         {
-            return _hasFirstReadTypeMeta ? _firstReadTypeMeta : null;
+            return _hasFirstTypeMetaRef ? _firstTypeMetaRef : null;
         }
 
-        return _readTypeMetas.Get(index - 1);
+        return _typeMetaRefs.Get(index - 1);
     }
 
-    internal void StoreReadTypeMeta(TypeMeta typeMeta, int index)
+    internal void StoreTypeMetaRef(TypeMeta typeMeta, int index)
     {
         if (index < 0)
         {
@@ -105,47 +100,44 @@ public sealed class ReadContext
 
         if (index == 0)
         {
-            _firstReadTypeMeta = typeMeta;
-            _hasFirstReadTypeMeta = true;
+            _firstTypeMetaRef = typeMeta;
+            _hasFirstTypeMetaRef = true;
             return;
         }
 
-        if (!_hasFirstReadTypeMeta)
+        if (!_hasFirstTypeMetaRef)
         {
             throw new InvalidDataException(
                 $"type meta index gap: index={index}, missing index 0");
         }
 
         int listIndex = index - 1;
-        if (listIndex == _readTypeMetas.Count)
+        if (listIndex == _typeMetaRefs.Count)
         {
-            _readTypeMetas.Add(typeMeta);
+            _typeMetaRefs.Add(typeMeta);
             return;
         }
 
-        if (listIndex < _readTypeMetas.Count)
+        if (listIndex < _typeMetaRefs.Count)
         {
-            _readTypeMetas.Set(listIndex, typeMeta);
+            _typeMetaRefs.Set(listIndex, typeMeta);
             return;
         }
 
         throw new InvalidDataException(
-            $"type meta index gap: index={index}, count={_readTypeMetas.Count + 1}");
+            $"type meta index gap: index={index}, count={_typeMetaRefs.Count + 1}");
     }
 
-    internal bool TryGetCachedReadTypeMeta(ulong header, out TypeMeta typeMeta)
+    internal bool TryGetTypeMetaByHeader(ulong header, out TypeMeta typeMeta)
     {
-        if (_hasLastMetaHeader && _lastMetaHeader == header && _lastTypeMeta is not null)
+        // UInt64Map reserves ulong.MaxValue as its empty-slot marker. A valid
+        // cached TypeMeta header cannot use reserved global-header bits, but an
+        // attacker-controlled cache lookup can happen before cold-path header
+        // validation, so this value must be forced to the miss path.
+        if (header != ulong.MaxValue &&
+            _typeMetasByHeader.TryGetValue(header, out TypeMeta? cached) &&
+            cached is not null)
         {
-            typeMeta = _lastTypeMeta;
-            return true;
-        }
-
-        if (_cachedTypeMetasByHeader.TryGetValue(header, out TypeMeta? cached) && cached is not null)
-        {
-            _lastMetaHeader = header;
-            _lastTypeMeta = cached;
-            _hasLastMetaHeader = true;
             typeMeta = cached;
             return true;
         }
@@ -154,25 +146,71 @@ public sealed class ReadContext
         return false;
     }
 
-    internal void CacheReadTypeMeta(ulong header, TypeMeta typeMeta)
+    internal void StoreRemoteTypeMeta(ulong header, TypeMeta typeMeta)
     {
-        if (_cachedTypeMetasByHeader.TryGetValue(header, out TypeMeta? existing) && existing is not null)
+        if (_typeMetasByHeader.TryGetValue(header, out _))
         {
-            _lastMetaHeader = header;
-            _lastTypeMeta = existing;
-            _hasLastMetaHeader = true;
             return;
         }
 
-        if (_cachedTypeMetasByHeader.Count >= MaxParsedTypeMetaEntries)
+        object typeKey = CheckRemoteTypeMetaLimits(typeMeta);
+        _typeMetasByHeader.Set(header, typeMeta);
+        RecordRemoteTypeMetaVersion(typeKey);
+    }
+
+    internal void StoreExactLocalTypeMeta(ulong header, TypeMeta typeMeta)
+    {
+        _typeMetasByHeader.Set(header, typeMeta);
+    }
+
+    [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)]
+    private object CheckRemoteTypeMetaLimits(TypeMeta typeMeta)
+    {
+        object typeKey;
+        if (typeMeta.RegisterByName)
         {
-            return;
+            typeKey = $"{typeMeta.NamespaceName.Value}\0{typeMeta.TypeName.Value}";
+        }
+        else if (typeMeta.UserTypeId.HasValue)
+        {
+            typeKey = typeMeta.UserTypeId.Value;
+        }
+        else
+        {
+            throw new InvalidDataException("remote metadata is missing type identity");
         }
+        _remoteSchemaVersionsByType.TryGetValue(typeKey, out int versionsForType);
+        int maxSchemaVersionsPerType = _config.MaxSchemaVersionsPerType;
+        if (versionsForType >= maxSchemaVersionsPerType)
+        {
+            throw new InvalidDataException(
+                $"Remote schema version limit exceeded for type {typeKey}: {versionsForType} >= {maxSchemaVersionsPerType}. " +
+                "The data may be malicious. If the data is not malicious, please increase MaxSchemaVersionsPerType.");
+        }
+
+        int acceptedTypeCount = versionsForType == 0
+            ? _remoteSchemaVersionsByType.Count + 1
+            : _remoteSchemaVersionsByType.Count;
+        int maxAverageSchemaVersionsPerType = _config.MaxAverageSchemaVersionsPerType;
+        long globalLimit = Math.Max(
+            MinRemoteTypeMetaLimit,
+            (long)acceptedTypeCount * maxAverageSchemaVersionsPerType);
+        if (_totalAcceptedSchemaVersions >= globalLimit)
+        {
+            throw new InvalidDataException(
+                $"Remote schema version limit exceeded: {_totalAcceptedSchemaVersions} metadata versions for " +
+                $"{acceptedTypeCount} accepted remote types exceeds the average limit {maxAverageSchemaVersionsPerType}. " +
+                "The data may be malicious. If the data is not malicious, please increase MaxAverageSchemaVersionsPerType.");
+        }
+
+        return typeKey;
+    }
 
-        _lastMetaHeader = header;
-        _lastTypeMeta = typeMeta;
-        _hasLastMetaHeader = true;
-        _cachedTypeMetasByHeader.TryAdd(header, typeMeta);
+    private void RecordRemoteTypeMetaVersion(object typeKey)
+    {
+        _remoteSchemaVersionsByType.TryGetValue(typeKey, out int versionsForType);
+        _remoteSchemaVersionsByType[typeKey] = versionsForType + 1;
+        _totalAcceptedSchemaVersions++;
     }
 
     internal MetaString? GetReadMetaString(int index)
@@ -186,37 +224,83 @@ public sealed class ReadContext
     }
 
     internal TypeMeta ReadTypeMeta()
+    {
+        if (TryReadTypeMetaRef(out int index, out TypeMeta typeMeta))
+        {
+            return typeMeta;
+        }
+
+        ulong header = Reader.ReadUInt64();
+        if (TryGetTypeMetaByHeader(header, out TypeMeta cachedTypeMeta))
+        {
+            // Header-cache hits intentionally skip without rehashing. Entries reach this cache only
+            // after successful TypeMeta body validation. Do not add body/hash/schema-limit/exact-local
+            // checks here; the miss path owns them before cache publish.
+            TypeMeta.SkipBody(Reader, header);
+            StoreTypeMetaRef(cachedTypeMeta, index);
+            return cachedTypeMeta;
+        }
+
+        Reader.MoveBack(sizeof(ulong));
+        int typeMetaStart = Reader.Cursor;
+        typeMeta = DecodeTypeMeta();
+        int typeMetaEnd = Reader.Cursor;
+        if (MatchesExactLocalTypeMeta(typeMeta, typeMetaStart, typeMetaEnd))
+        {
+            StoreExactLocalTypeMeta(header, typeMeta);
+        }
+        else
+        {
+            StoreRemoteTypeMeta(header, typeMeta);
+        }
+        StoreTypeMetaRef(typeMeta, index);
+        return typeMeta;
+    }
+
+    internal bool TryReadTypeMetaRef(out int index, out TypeMeta typeMeta)
     {
         uint indexMarker = Reader.ReadVarUInt32();
         bool isRef = (indexMarker & 1) == 1;
-        int index = checked((int)(indexMarker >> 1));
+        index = checked((int)(indexMarker >> 1));
         if (isRef)
         {
-            TypeMeta? cached = GetReadTypeMeta(index);
+            TypeMeta? cached = GetTypeMetaRef(index);
             if (cached is null)
             {
                 throw new InvalidDataException($"unknown type meta ref index {index}");
             }
 
-            return cached;
+            typeMeta = cached;
+            return true;
         }
 
-        ulong header = Reader.ReadUInt64();
-        if (TryGetCachedReadTypeMeta(header, out TypeMeta cachedTypeMeta))
+        typeMeta = null!;
+        return false;
+    }
+
+    [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)]
+    internal bool MatchesExactLocalTypeMeta(TypeMeta typeMeta, int start, int end)
+    {
+        if (!TypeResolver.TryGetLocalTypeInfo(typeMeta, out TypeInfo exactLocal))
         {
-            // Header-cache hits intentionally skip without rehashing. Entries reach this cache only
-            // after a successful TypeMeta parse and 52-bit metadata-hash validation. The current body
-            // size still comes from the current header bytes, not from the cached TypeMeta.
-            TypeMeta.SkipBody(Reader, header);
-            StoreReadTypeMeta(cachedTypeMeta, index);
-            return cachedTypeMeta;
+            return false;
         }
 
-        Reader.MoveBack(sizeof(ulong));
-        TypeMeta typeMeta = TypeMeta.Decode(Reader);
-        StoreReadTypeMeta(typeMeta, index);
-        CacheReadTypeMeta(header, typeMeta);
-        return typeMeta;
+        TypeInfo.TypeMetaCacheEntry local = exactLocal.GetTypeMetaCacheEntry(TrackRef);
+        byte[] encoded = local.EncodedBytes;
+        if (end - start != encoded.Length ||
+            !Reader.Storage.AsSpan(start, encoded.Length).SequenceEqual(encoded))
+        {
+            return false;
+        }
+
+        return true;
+    }
+
+    [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)]
+    internal TypeMeta DecodeTypeMeta()
+    {
+        return TypeMeta.Decode(Reader, _config.MaxTypeFields, _config.MaxTypeMetaBytes);
     }
 
     internal void StoreTypeMeta(Type type, TypeMeta typeMeta)
@@ -372,9 +456,9 @@ public sealed class ReadContext
         _cachedTypeMetaType = null;
         _cachedTypeMeta = null;
         _currentDynamicReadDepth = 0;
-        _firstReadTypeMeta = null;
-        _hasFirstReadTypeMeta = false;
-        _readTypeMetas.Clear();
+        _firstTypeMetaRef = null;
+        _hasFirstTypeMetaRef = false;
+        _typeMetaRefs.Clear();
         _readMetaStrings.Clear();
     }
 }
diff --git a/csharp/src/Fory/TypeInfo.cs b/csharp/src/Fory/TypeInfo.cs
index 99972bdb6..8341d418c 100644
--- a/csharp/src/Fory/TypeInfo.cs
+++ b/csharp/src/Fory/TypeInfo.cs
@@ -748,6 +748,15 @@ public sealed class TypeInfo
         byte[] encoded = typeMeta.Encode();
         ulong header = BinaryPrimitives.ReadUInt64LittleEndian(encoded);
         ulong headerHash = header >> TypeMetaConstants.TypeMetaHashShift;
+        typeMeta = new TypeMeta(
+            typeMeta.TypeId,
+            typeMeta.UserTypeId,
+            typeMeta.NamespaceName,
+            typeMeta.TypeName,
+            typeMeta.RegisterByName,
+            typeMeta.Fields,
+            typeMeta.Compressed,
+            headerHash);
         return new TypeMetaCacheEntry(typeMeta, encoded, headerHash);
     }
 }
diff --git a/csharp/src/Fory/TypeMeta.cs b/csharp/src/Fory/TypeMeta.cs
index 31ea8e29a..322bd6222 100644
--- a/csharp/src/Fory/TypeMeta.cs
+++ b/csharp/src/Fory/TypeMeta.cs
@@ -394,6 +394,9 @@ public sealed class TypeMetaFieldInfo : IEquatable<TypeMetaFieldInfo>
 
 public sealed class TypeMeta : IEquatable<TypeMeta>
 {
+    private const int DefaultMaxTypeFields = 512;
+    private const int DefaultMaxTypeMetaBytes = 4096;
+
     private bool _hasAssignedFieldIds;
 
     public TypeMeta(
@@ -486,14 +489,23 @@ public sealed class TypeMeta : IEquatable<TypeMeta>
 
     public static TypeMeta Decode(byte[] bytes)
     {
-        return Decode(new ByteReader(bytes));
+        return Decode(new ByteReader(bytes), DefaultMaxTypeFields, DefaultMaxTypeMetaBytes);
     }
 
     public static TypeMeta Decode(ByteReader reader)
+    {
+        return Decode(reader, DefaultMaxTypeFields, DefaultMaxTypeMetaBytes);
+    }
+
+    internal static TypeMeta Decode(ByteReader reader, int maxTypeFields, int maxTypeMetaBytes)
     {
         ulong header = reader.ReadUInt64();
         ValidateGlobalHeader(header);
         int metaSize = ReadBodySize(reader, header);
+        CheckBodySize(metaSize, maxTypeMetaBytes);
+        // Keep ReadBytes as the first body materialization: it checks the
+        // reader bound before allocating/copying. The size limit alone does
+        // not prove the declared bytes exist in the input.
         byte[] encodedBody = reader.ReadBytes(metaSize);
         ByteReader bodyReader = new(encodedBody);
         byte metaHeader = bodyReader.ReadUInt8();
@@ -516,6 +528,7 @@ public sealed class TypeMeta : IEquatable<TypeMeta>
             {
                 numFields += (int)bodyReader.ReadVarUInt32();
             }
+            CheckFieldCount(numFields, maxTypeFields);
         }
         else
         {
@@ -570,14 +583,6 @@ public sealed class TypeMeta : IEquatable<TypeMeta>
             header >> TypeMetaConstants.TypeMetaHashShift);
     }
 
-    internal static void ValidateAndSkipBody(ByteReader reader, ulong header)
-    {
-        ValidateGlobalHeader(header);
-        int metaSize = ReadBodySize(reader, header);
-        ReadOnlySpan<byte> encodedBody = reader.ReadSpan(metaSize);
-        ValidateParsedTypeMetaHash(header, encodedBody);
-    }
-
     private static void ValidateGlobalHeader(ulong header)
     {
         if ((header & TypeMetaConstants.TypeMetaReservedFlags) != 0)
@@ -608,6 +613,24 @@ public sealed class TypeMeta : IEquatable<TypeMeta>
         return metaSize;
     }
 
+    private static void CheckBodySize(int metaSize, int maxTypeMetaBytes)
+    {
+        if (metaSize > maxTypeMetaBytes)
+        {
+            throw new InvalidDataException(
+                $"Type metadata body size {metaSize} exceeds MaxTypeMetaBytes {maxTypeMetaBytes}. The data may be malicious. If the data is not malicious, please increase MaxTypeMetaBytes.");
+        }
+    }
+
+    internal static void CheckFieldCount(int numFields, int maxTypeFields)
+    {
+        if (numFields > maxTypeFields)
+        {
+            throw new InvalidDataException(
+                $"Type metadata field count {numFields} exceeds MaxTypeFields {maxTypeFields}. The data may be malicious. If the data is not malicious, please increase MaxTypeFields.");
+        }
+    }
+
     internal static void SkipBody(ByteReader reader, ulong header)
     {
         reader.Skip(ReadBodySize(reader, header));
diff --git a/csharp/src/Fory/TypeResolver.cs b/csharp/src/Fory/TypeResolver.cs
index 8313fed75..a5cb888c4 100644
--- a/csharp/src/Fory/TypeResolver.cs
+++ b/csharp/src/Fory/TypeResolver.cs
@@ -697,18 +697,36 @@ public sealed class TypeResolver
             !info.RegisterByName &&
             typeId == TypeId.CompatibleStruct)
         {
-            TypeMeta remoteTypeMeta = context.ReadTypeMeta();
-            if (!HasValidatedTypeMeta(info, remoteTypeMeta))
+            TypeMeta remoteTypeMeta;
+            if (context.TryReadTypeMetaRef(out int index, out remoteTypeMeta))
             {
-                ValidateTypeMeta(
-                    remoteTypeMeta,
-                    info,
-                    info.UserTypeKind.Value,
-                    info.RegisterByName,
-                    context.Compatible,
-                    typeId);
-                remoteTypeMeta.EnsureAssignedFieldIds(TypeMetaFields(info, context.TrackRef));
-                SetValidatedTypeMeta(info, remoteTypeMeta);
+                if (!HasValidatedTypeMeta(info, remoteTypeMeta))
+                {
+                    ValidateRemoteTypeMeta(remoteTypeMeta, info, typeId, assignFieldIds: true, context);
+                }
+            }
+            else
+            {
+                ulong header = context.Reader.ReadUInt64();
+                if (context.TryGetTypeMetaByHeader(header, out remoteTypeMeta))
+                {
+                    TypeMeta.SkipBody(context.Reader, header);
+                    context.StoreTypeMetaRef(remoteTypeMeta, index);
+                    if (!HasValidatedTypeMeta(info, remoteTypeMeta))
+                    {
+                        ValidateRemoteTypeMeta(remoteTypeMeta, info, typeId, assignFieldIds: true, context);
+                    }
+                }
+                else
+                {
+                    remoteTypeMeta = ReadRemoteTypeMeta(
+                        info,
+                        typeId,
+                        assignFieldIds: true,
+                        context,
+                        index,
+                        header);
+                }
             }
 
             context.StoreTypeMeta(type, remoteTypeMeta);
@@ -729,18 +747,36 @@ public sealed class TypeResolver
             case TypeId.CompatibleStruct:
             case TypeId.NamedCompatibleStruct:
                 {
-                    TypeMeta remoteTypeMeta = context.ReadTypeMeta();
-                    if (!HasValidatedTypeMeta(info, remoteTypeMeta))
+                    TypeMeta remoteTypeMeta;
+                    if (context.TryReadTypeMetaRef(out int index, out remoteTypeMeta))
+                    {
+                        if (!HasValidatedTypeMeta(info, remoteTypeMeta))
+                        {
+                            ValidateRemoteTypeMeta(remoteTypeMeta, info, typeId, assignFieldIds: true, context);
+                        }
+                    }
+                    else
                     {
-                        ValidateTypeMeta(
-                            remoteTypeMeta,
-                            info,
-                            declaredKind,
-                            registerByName,
-                            compatible,
-                            typeId);
-                        remoteTypeMeta.EnsureAssignedFieldIds(TypeMetaFields(info, context.TrackRef));
-                        SetValidatedTypeMeta(info, remoteTypeMeta);
+                        ulong header = context.Reader.ReadUInt64();
+                        if (context.TryGetTypeMetaByHeader(header, out remoteTypeMeta))
+                        {
+                            TypeMeta.SkipBody(context.Reader, header);
+                            context.StoreTypeMetaRef(remoteTypeMeta, index);
+                            if (!HasValidatedTypeMeta(info, remoteTypeMeta))
+                            {
+                                ValidateRemoteTypeMeta(remoteTypeMeta, info, typeId, assignFieldIds: true, context);
+                            }
+                        }
+                        else
+                        {
+                            remoteTypeMeta = ReadRemoteTypeMeta(
+                                info,
+                                typeId,
+                                assignFieldIds: true,
+                                context,
+                                index,
+                                header);
+                        }
                     }
 
                     context.StoreTypeMeta(type, remoteTypeMeta);
@@ -781,17 +817,35 @@ public sealed class TypeResolver
     {
         if (compatible)
         {
-            TypeMeta remoteTypeMeta = context.ReadTypeMeta();
-            if (!HasValidatedTypeMeta(typeInfo, remoteTypeMeta))
+            if (context.TryReadTypeMetaRef(out int index, out TypeMeta remoteTypeMeta))
             {
-                ValidateTypeMeta(
-                    remoteTypeMeta,
-                    typeInfo,
-                    declaredKind,
-                    registerByName,
-                    compatible,
-                    wireTypeId);
-                SetValidatedTypeMeta(typeInfo, remoteTypeMeta);
+                if (!HasValidatedTypeMeta(typeInfo, remoteTypeMeta))
+                {
+                    ValidateRemoteTypeMeta(remoteTypeMeta, typeInfo, wireTypeId, assignFieldIds: false, context);
+                }
+            }
+            else
+            {
+                ulong header = context.Reader.ReadUInt64();
+                if (context.TryGetTypeMetaByHeader(header, out remoteTypeMeta))
+                {
+                    TypeMeta.SkipBody(context.Reader, header);
+                    context.StoreTypeMetaRef(remoteTypeMeta, index);
+                    if (!HasValidatedTypeMeta(typeInfo, remoteTypeMeta))
+                    {
+                        ValidateRemoteTypeMeta(remoteTypeMeta, typeInfo, wireTypeId, assignFieldIds: false, context);
+                    }
+                }
+                else
+                {
+                    _ = ReadRemoteTypeMeta(
+                        typeInfo,
+                        wireTypeId,
+                        assignFieldIds: false,
+                        context,
+                        index,
+                        header);
+                }
             }
 
             return;
@@ -818,6 +872,63 @@ public sealed class TypeResolver
         }
     }
 
+    [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)]
+    private TypeMeta ReadRemoteTypeMeta(
+        TypeInfo typeInfo,
+        TypeId wireTypeId,
+        bool assignFieldIds,
+        ReadContext context,
+        int index,
+        ulong header)
+    {
+        context.Reader.MoveBack(sizeof(ulong));
+        int typeMetaStart = context.Reader.Cursor;
+        TypeMeta remoteTypeMeta = context.DecodeTypeMeta();
+        int typeMetaEnd = context.Reader.Cursor;
+        if (!HasValidatedTypeMeta(typeInfo, remoteTypeMeta))
+        {
+            ValidateRemoteTypeMeta(
+                remoteTypeMeta,
+                typeInfo,
+                wireTypeId,
+                assignFieldIds,
+                context);
+        }
+        if (context.MatchesExactLocalTypeMeta(remoteTypeMeta, typeMetaStart, typeMetaEnd))
+        {
+            context.StoreExactLocalTypeMeta(header, remoteTypeMeta);
+        }
+        else
+        {
+            context.StoreRemoteTypeMeta(header, remoteTypeMeta);
+        }
+        context.StoreTypeMetaRef(remoteTypeMeta, index);
+        return remoteTypeMeta;
+    }
+
+    [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)]
+    private void ValidateRemoteTypeMeta(
+        TypeMeta remoteTypeMeta,
+        TypeInfo typeInfo,
+        TypeId wireTypeId,
+        bool assignFieldIds,
+        ReadContext context)
+    {
+        ValidateTypeMeta(
+            remoteTypeMeta,
+            typeInfo,
+            typeInfo.UserTypeKind!.Value,
+            typeInfo.RegisterByName,
+            context.Compatible,
+            wireTypeId);
+        if (assignFieldIds)
+        {
+            remoteTypeMeta.EnsureAssignedFieldIds(TypeMetaFields(typeInfo, context.TrackRef));
+        }
+
+        SetValidatedTypeMeta(typeInfo, remoteTypeMeta);
+    }
+
     internal static TypeId ResolveWireTypeId(
         UserTypeKind declaredKind,
         bool registerByName,
@@ -898,7 +1009,7 @@ public sealed class TypeResolver
         {
             case TypeId.CompatibleStruct:
             case TypeId.NamedCompatibleStruct:
-                return ResolveAnyTypeInfoFromMeta(wireTypeId, context.ReadTypeMeta(), context.Compatible);
+                return ReadAnyTypeInfo(wireTypeId, context.Compatible, context);
             case TypeId.NamedEnum:
             case TypeId.NamedStruct:
             case TypeId.NamedExt:
@@ -918,7 +1029,7 @@ public sealed class TypeResolver
     {
         if (context.Compatible)
         {
-            return ResolveAnyTypeInfoFromMeta(wireTypeId, context.ReadTypeMeta(), compatible: true);
+            return ReadAnyTypeInfo(wireTypeId, compatible: true, context);
         }
 
         MetaString namespaceName = ReadMetaString(
@@ -932,6 +1043,49 @@ public sealed class TypeResolver
         return ResolveAnyUserTypeInfo(wireTypeId, namespaceName.Value, typeName.Value, compatible: false);
     }
 
+    private TypeInfo ReadAnyTypeInfo(TypeId wireTypeId, bool compatible, ReadContext context)
+    {
+        if (context.TryReadTypeMetaRef(out int index, out TypeMeta typeMeta))
+        {
+            TypeInfo typeInfo = ResolveAnyTypeInfoFromMeta(wireTypeId, typeMeta, compatible);
+            typeMeta.EnsureAssignedFieldIds(TypeMetaFields(typeInfo, context.TrackRef));
+            context.StoreTypeMeta(typeInfo.Type, typeMeta);
+            return typeInfo;
+        }
+
+        int typeMetaStart = context.Reader.Cursor;
+        ulong header = context.Reader.ReadUInt64();
+        if (context.TryGetTypeMetaByHeader(header, out typeMeta))
+        {
+            // Header-cache hits intentionally skip without rehashing. Entries reach this cache only
+            // after successful TypeMeta body validation. Do not add body/hash/schema-limit/exact-local
+            // checks here; Any resolution and field binding below are existing read work.
+            TypeMeta.SkipBody(context.Reader, header);
+            context.StoreTypeMetaRef(typeMeta, index);
+            TypeInfo typeInfo = ResolveAnyTypeInfoFromMeta(wireTypeId, typeMeta, compatible);
+            typeMeta.EnsureAssignedFieldIds(TypeMetaFields(typeInfo, context.TrackRef));
+            context.StoreTypeMeta(typeInfo.Type, typeMeta);
+            return typeInfo;
+        }
+
+        context.Reader.MoveBack(sizeof(ulong));
+        typeMeta = context.DecodeTypeMeta();
+        int typeMetaEnd = context.Reader.Cursor;
+        TypeInfo resolvedInfo = ResolveAnyTypeInfoFromMeta(wireTypeId, typeMeta, compatible);
+        typeMeta.EnsureAssignedFieldIds(TypeMetaFields(resolvedInfo, context.TrackRef));
+        if (context.MatchesExactLocalTypeMeta(typeMeta, typeMetaStart, typeMetaEnd))
+        {
+            context.StoreExactLocalTypeMeta(header, typeMeta);
+        }
+        else
+        {
+            context.StoreRemoteTypeMeta(header, typeMeta);
+        }
+        context.StoreTypeMetaRef(typeMeta, index);
+        context.StoreTypeMeta(resolvedInfo.Type, typeMeta);
+        return resolvedInfo;
+    }
+
     internal object? ReadAnyValue(TypeInfo typeInfo, ReadContext context)
     {
         TypeId wireTypeId = typeInfo.WireTypeId
@@ -1116,6 +1270,24 @@ public sealed class TypeResolver
         throw new TypeNotRegisteredException($"namespace={namespaceName}, type={typeName}");
     }
 
+    internal bool TryGetLocalTypeInfo(TypeMeta typeMeta, out TypeInfo typeInfo)
+    {
+        if (typeMeta.RegisterByName)
+        {
+            return _byTypeName.TryGetValue(
+                (typeMeta.NamespaceName.Value, typeMeta.TypeName.Value),
+                out typeInfo!);
+        }
+
+        if (typeMeta.UserTypeId.HasValue)
+        {
+            return _byUserTypeId.TryGetValue(typeMeta.UserTypeId.Value, out typeInfo!);
+        }
+
+        typeInfo = null!;
+        return false;
+    }
+
     private static byte[] ReadBinary(ReadContext context)
     {
         uint length = context.Reader.ReadVarUInt32();
diff --git a/csharp/tests/Fory.Tests/ForyRuntimeTests.cs b/csharp/tests/Fory.Tests/ForyRuntimeTests.cs
index b8c06f695..6d9300394 100644
--- a/csharp/tests/Fory.Tests/ForyRuntimeTests.cs
+++ b/csharp/tests/Fory.Tests/ForyRuntimeTests.cs
@@ -85,6 +85,33 @@ public sealed class NonPrimitiveFieldOrder
     public int IntValue { get; set; }
 }
 
+public sealed class LateTypeMetaExt
+{
+    public int Value { get; set; }
+}
+
+public sealed class LateTypeMetaExtSerializer : Serializer<LateTypeMetaExt>
+{
+    public override LateTypeMetaExt DefaultValue => null!;
+
+    public override void WriteData(WriteContext context, in LateTypeMetaExt value, bool hasGenerics)
+    {
+        _ = hasGenerics;
+        context.Writer.WriteVarInt32(value?.Value ?? 0);
+    }
+
+    public override LateTypeMetaExt ReadData(ReadContext context)
+    {
+        return new LateTypeMetaExt { Value = context.Reader.ReadVarInt32() };
+    }
+}
+
+[ForyStruct]
+public sealed class LateTypeMetaHolder
+{
+    public LateTypeMetaExt Value { get; set; } = new();
+}
+
 [ForyStruct]
 public sealed class SchemaNumbers
 {
@@ -1118,7 +1145,7 @@ public sealed class ForyRuntimeTests
         short first = reader.ReadInt16();
         long second = reader.ReadVarInt64();
         int third = reader.ReadVarInt32();
-        ReadContext tailContext = new(reader, new TypeResolver(), false, false);
+        ReadContext tailContext = new(reader, new TypeResolver(), fory.Config);
         string fourth = tailContext.TypeResolver.GetSerializer<string>().ReadData(tailContext);
 
         Assert.Equal(value.B, first);
@@ -1141,6 +1168,14 @@ public sealed class ForyRuntimeTests
             fieldNames);
     }
 
+    [Fact]
+    public void TypeMetaUsesLateRegisteredFieldType()
+    {
+        Assert.Equal(
+            LateHolderTypeMetaBytes(registerExtFirst: true),
+            LateHolderTypeMetaBytes(registerExtFirst: false));
+    }
+
     [Fact]
     public void ForyFieldSchemaTypeOverridesForUnsignedTypes()
     {
@@ -2134,6 +2169,24 @@ public sealed class ForyRuntimeTests
         Assert.Equal("future", unknownCase.Value.Value);
     }
 
+    [Fact]
+    public void IdUnionDoesNotUseTypeMetaLimits()
+    {
+        ForyRuntime fory = ForyRuntime.Builder()
+            .Compatible(true)
+            .MaxTypeMetaBytes(1)
+            .MaxSchemaVersionsPerType(1)
+            .Build();
+        fory.Register<SourceGeneratedShape>(904);
+
+        SourceGeneratedShape value = new SourceGeneratedShape.Number(42);
+        SourceGeneratedShape decoded =
+            fory.Deserialize<SourceGeneratedShape>(fory.Serialize(value));
+
+        SourceGeneratedShape.Number number = Assert.IsType<SourceGeneratedShape.Number>(decoded);
+        Assert.Equal(42, number.Value);
+    }
+
     [Fact]
     public void UnionFieldRoundTripCompatible()
     {
@@ -2157,6 +2210,7 @@ public sealed class ForyRuntimeTests
     {
         TypeResolver resolver = new();
         Serializer<Union2<string, long>> serializer = resolver.GetSerializer<Union2<string, long>>();
+        Config config = ForyRuntime.Builder().TrackRef(true).Build().Config;
 
         ByteWriter firstWriter = new();
         WriteContext firstWrite = new(firstWriter, resolver, trackRef: true);
@@ -2165,7 +2219,7 @@ public sealed class ForyRuntimeTests
         Assert.Equal(0u, firstReader.ReadVarUInt32());
 
         Union2<string, long> firstDecoded =
-            serializer.ReadData(new ReadContext(new ByteReader(firstWriter.ToArray()), resolver, trackRef: true));
+            serializer.ReadData(new ReadContext(new ByteReader(firstWriter.ToArray()), resolver, config));
         Assert.Equal(0, firstDecoded.Index);
         Assert.Equal("hello", firstDecoded.GetT1());
 
@@ -2176,7 +2230,7 @@ public sealed class ForyRuntimeTests
         Assert.Equal(1u, secondReader.ReadVarUInt32());
 
         Union2<string, long> secondDecoded =
-            serializer.ReadData(new ReadContext(new ByteReader(secondWriter.ToArray()), resolver, trackRef: true));
+            serializer.ReadData(new ReadContext(new ByteReader(secondWriter.ToArray()), resolver, config));
         Assert.Equal(1, secondDecoded.Index);
         Assert.Equal(42L, secondDecoded.GetT2());
 
@@ -2857,7 +2911,8 @@ public sealed class ForyRuntimeTests
         int byteLength = checked((int)(header >> 2));
         Assert.Equal(payload.Length - headerReader.Cursor, byteLength);
 
-        ReadContext readContext = new(new ByteReader(payload), resolver, trackRef: false, compatible: false);
+        Config config = ForyRuntime.Builder().Compatible(false).Build().Config;
+        ReadContext readContext = new(new ByteReader(payload), resolver, config);
         string decoded = StringSerializer.ReadString(readContext);
         Assert.Equal(0, readContext.Reader.Remaining);
         return (encoding, decoded);
@@ -2874,4 +2929,29 @@ public sealed class ForyRuntimeTests
         Assert.Equal(expected.U32Nullable, actual.U32Nullable);
         Assert.Equal(expected.U64Nullable, actual.U64Nullable);
     }
+
+    private static byte[] LateHolderTypeMetaBytes(bool registerExtFirst)
+    {
+        TypeResolver resolver = new();
+        if (registerExtFirst)
+        {
+            RegisterLateTypeMetaExt(resolver);
+        }
+
+        resolver.Register(typeof(LateTypeMetaHolder), "example", "LateTypeMetaHolder");
+        if (!registerExtFirst)
+        {
+            RegisterLateTypeMetaExt(resolver);
+        }
+
+        return resolver.GetTypeInfo(typeof(LateTypeMetaHolder))
+            .GetTypeMetaCacheEntry(trackRef: false)
+            .EncodedBytes;
+    }
+
+    private static void RegisterLateTypeMetaExt(TypeResolver resolver)
+    {
+        TypeInfo typeInfo = TypeInfo.Create(typeof(LateTypeMetaExt), new LateTypeMetaExtSerializer());
+        resolver.Register(typeof(LateTypeMetaExt), "example", "LateTypeMetaExt", typeInfo);
+    }
 }
diff --git a/csharp/tests/Fory.Tests/RuntimeEdgeCaseTests.cs b/csharp/tests/Fory.Tests/RuntimeEdgeCaseTests.cs
index 78b57e8ef..bec43a4a5 100644
--- a/csharp/tests/Fory.Tests/RuntimeEdgeCaseTests.cs
+++ b/csharp/tests/Fory.Tests/RuntimeEdgeCaseTests.cs
@@ -206,7 +206,8 @@ public sealed class RuntimeEdgeCaseTests
 
         writer.WriteUInt8(0xA5);
         ByteReader reader = new(writer.ToArray());
-        ReadContext context = new(reader, new TypeResolver(), trackRef: false);
+        Config config = ForyRuntime.Builder().Compatible(false).Build().Config;
+        ReadContext context = new(reader, new TypeResolver(), config);
 
         FieldSkipper.SkipFieldValue(context, new TypeMetaFieldType((uint)typeId, nullable: false));
 
@@ -525,26 +526,179 @@ public sealed class RuntimeEdgeCaseTests
     }
 
     [Fact]
-    public void TypeMetaHeaderCacheStopsPublishingAtCapacity()
+    public void TypeMetaSchemaLimitRejectsExtraVersions()
     {
-        ReadContext context = new(new ByteReader(Array.Empty<byte>()), new TypeResolver(), trackRef: false);
-        TypeMeta typeMeta = new(
-            (uint)TypeId.Struct,
-            901,
-            MetaString.Empty('.', '_'),
-            MetaString.Empty('$', '_'),
-            registerByName: false,
-            []);
+        Config config = ForyRuntime.Builder()
+            .Compatible(false)
+            .MaxSchemaVersionsPerType(1)
+            .Build()
+            .Config;
+        ReadContext context = new(new ByteReader(Array.Empty<byte>()), new TypeResolver(), config);
+        TypeMeta first = RemoteStructTypeMeta(901, "first");
+        TypeMeta second = RemoteStructTypeMeta(901, "second");
 
-        for (ulong header = 1; header <= 8192; header++)
-        {
-            context.CacheReadTypeMeta(header, typeMeta);
-        }
+        ReadAndStoreTypeMeta(context, first);
+
+        Assert.Throws<InvalidDataException>(() => ReadAndStoreTypeMeta(context, second));
+    }
+
+    [Fact]
+    public void NonStructTypeMetaUsesSchemaLimit()
+    {
+        Config config = ForyRuntime.Builder()
+            .Compatible(false)
+            .MaxSchemaVersionsPerType(1)
+            .Build()
+            .Config;
+        ReadContext context = new(new ByteReader(Array.Empty<byte>()), new TypeResolver(), config);
+        TypeMeta first = RemoteNamedNonStructTypeMeta(TypeId.NamedEnum, "SharedEnum");
+        TypeMeta second = RemoteNamedNonStructTypeMeta(TypeId.NamedExt, "SharedEnum");
+
+        ReadAndStoreTypeMeta(context, first);
+
+        InvalidDataException exception =
+            Assert.Throws<InvalidDataException>(() => ReadAndStoreTypeMeta(context, second));
+        Assert.Contains("MaxSchemaVersionsPerType", exception.Message, StringComparison.Ordinal);
+    }
+
+    [Fact]
+    public void IdEnumDoesNotUseTypeMetaLimits()
+    {
+        ForyRuntime fory = ForyRuntime.Builder()
+            .Compatible(true)
+            .MaxTypeMetaBytes(1)
+            .MaxSchemaVersionsPerType(1)
+            .Build();
+        fory.Register<TestColor>(901);
+
+        Assert.Equal(TestColor.Red, fory.Deserialize<TestColor>(fory.Serialize(TestColor.Red)));
+    }
+
+    [Fact]
+    public void IdExtDoesNotUseTypeMetaLimits()
+    {
+        ForyRuntime fory = ForyRuntime.Builder()
+            .Compatible(true)
+            .MaxTypeMetaBytes(1)
+            .MaxSchemaVersionsPerType(1)
+            .Build();
+        fory.Register<CustomPayload, CustomPayloadSerializer>(902);
+
+        byte[] payload = fory.Serialize<object?>(new CustomPayload { Id = 9 });
+        object? decoded = fory.Deserialize<object?>(payload);
+
+        CustomPayload result = Assert.IsType<CustomPayload>(decoded);
+        Assert.Equal(9, result.Id);
+        Assert.Equal("custom", result.Marker);
+    }
+
+    [Fact]
+    public void TypeMetaFieldLimitRejectsLargeStruct()
+    {
+        Config config = ForyRuntime.Builder()
+            .Compatible(false)
+            .MaxTypeFields(1)
+            .Build()
+            .Config;
+        ReadContext context = new(new ByteReader(Array.Empty<byte>()), new TypeResolver(), config);
+        TypeMeta typeMeta = RemoteStructTypeMeta(901, "first", "second");
+
+        InvalidDataException exception =
+            Assert.Throws<InvalidDataException>(() => ReadAndStoreTypeMeta(context, typeMeta));
+        Assert.Contains("MaxTypeFields", exception.Message, StringComparison.Ordinal);
+    }
+
+    [Fact]
+    public void TypeMetaBodyLimitRejectsLargeMetadata()
+    {
+        Config config = ForyRuntime.Builder()
+            .Compatible(false)
+            .MaxTypeMetaBytes(1)
+            .Build()
+            .Config;
+        ReadContext context = new(new ByteReader(Array.Empty<byte>()), new TypeResolver(), config);
+
+        InvalidDataException exception =
+            Assert.Throws<InvalidDataException>(() => ReadAndStoreTypeMeta(context, RemoteStructTypeMeta(901, "value")));
+        Assert.Contains("MaxTypeMetaBytes", exception.Message, StringComparison.Ordinal);
+    }
+
+    [Fact]
+    public void TypeMetaSchemaLimitKeepsUnknownTypesSeparate()
+    {
+        Config config = ForyRuntime.Builder()
+            .Compatible(false)
+            .MaxSchemaVersionsPerType(1)
+            .Build()
+            .Config;
+        ReadContext context = new(new ByteReader(Array.Empty<byte>()), new TypeResolver(), config);
+        TypeMeta first = RemoteStructTypeMeta(901, "value");
+        TypeMeta second = RemoteStructTypeMeta(902, "value");
+
+        TypeMeta firstRead = ReadAndStoreTypeMeta(context, first);
+        TypeMeta secondRead = ReadAndStoreTypeMeta(context, second);
+
+        Assert.True(context.TryGetTypeMetaByHeader(EncodedTypeMetaHeader(firstRead), out _));
+        Assert.True(context.TryGetTypeMetaByHeader(EncodedTypeMetaHeader(secondRead), out _));
+    }
+
+    [Fact]
+    public void FailedAnyTypeMetaDoesNotConsumeLimit()
+    {
+        TypeResolver resolver = new();
+        resolver.Register(typeof(CustomPayload), 901);
+        Config config = ForyRuntime.Builder()
+            .Compatible(true)
+            .MaxSchemaVersionsPerType(1)
+            .Build()
+            .Config;
+        ReadContext context = new(new ByteReader(Array.Empty<byte>()), resolver, config);
+
+        Assert.Throws<InvalidDataException>(() =>
+            ReadAnyTypeInfo(context, resolver, RemoteCompatibleStructTypeMeta(901, "Id", MapType())));
+        TypeMeta accepted = ReadAndStoreTypeMeta(context, RemoteStructTypeMeta(901, "second"));
+
+        Assert.True(context.TryGetTypeMetaByHeader(EncodedTypeMetaHeader(accepted), out _));
+    }
 
-        Assert.True(context.TryGetCachedReadTypeMeta(8192, out _));
-        context.CacheReadTypeMeta(8193, typeMeta);
+    [Fact]
+    public void ExactAnyTypeMetaIsFree()
+    {
+        TypeResolver resolver = new();
+        resolver.Register(typeof(CustomPayload), 901);
+        Config config = ForyRuntime.Builder()
+            .Compatible(true)
+            .MaxSchemaVersionsPerType(1)
+            .Build()
+            .Config;
+        ReadContext context = new(new ByteReader(Array.Empty<byte>()), resolver, config);
+        TypeMeta remote = ReadAndStoreTypeMeta(context, RemoteStructTypeMeta(901, "remote"));
+        TypeMeta exact = resolver.GetTypeInfo(typeof(CustomPayload)).GetTypeMetaCacheEntry(trackRef: false).TypeMeta;
+
+        TypeInfo typeInfo = ReadAnyTypeInfo(context, resolver, exact);
+
+        Assert.True(context.TryGetTypeMetaByHeader(EncodedTypeMetaHeader(remote), out _));
+    }
 
-        Assert.False(context.TryGetCachedReadTypeMeta(8193, out _));
+    [Fact]
+    public void ExactNonStructTypeMetaBypassesLimit()
+    {
+        TypeResolver resolver = new();
+        resolver.Register(typeof(TestColor), "example", "SharedEnum");
+        Config config = ForyRuntime.Builder()
+            .Compatible(true)
+            .MaxSchemaVersionsPerType(1)
+            .Build()
+            .Config;
+        ReadContext context = new(new ByteReader(Array.Empty<byte>()), resolver, config);
+        TypeInfo typeInfo = resolver.GetTypeInfo(typeof(TestColor));
+        TypeMeta exact = typeInfo.GetTypeMetaCacheEntry(trackRef: false).TypeMeta;
+
+        ReadAndStoreTypeMeta(context, exact);
+
+        TypeMeta remote = RemoteNamedNonStructTypeMeta(TypeId.NamedExt, "SharedEnum");
+        ReadAndStoreTypeMeta(context, remote);
+        Assert.True(context.TryGetTypeMetaByHeader(EncodedTypeMetaHeader(remote), out _));
     }
 
     [Fact]
@@ -566,13 +720,105 @@ public sealed class RuntimeEdgeCaseTests
         writer.WriteBytes(new byte[0xff]);
         writer.WriteUInt8(0x7b);
 
-        ReadContext context = new(new ByteReader(writer.ToArray()), new TypeResolver(), trackRef: false);
-        context.CacheReadTypeMeta(header, typeMeta);
+        Config config = ForyRuntime.Builder().Compatible(false).Build().Config;
+        ReadContext context = new(new ByteReader(writer.ToArray()), new TypeResolver(), config);
+        context.StoreRemoteTypeMeta(header, typeMeta);
 
         Assert.Same(typeMeta, context.ReadTypeMeta());
         Assert.Equal(0x7b, context.Reader.ReadUInt8());
     }
 
+    private static TypeMeta RemoteStructTypeMeta(uint userTypeId, string fieldName)
+    {
+        return RemoteStructTypeMeta(userTypeId, [fieldName]);
+    }
+
+    private static TypeMeta RemoteStructTypeMeta(uint userTypeId, params string[] fieldNames)
+    {
+        TypeMetaFieldInfo[] fields = new TypeMetaFieldInfo[fieldNames.Length];
+        for (int i = 0; i < fieldNames.Length; i++)
+        {
+            fields[i] = new TypeMetaFieldInfo(null, fieldNames[i], new TypeMetaFieldType((uint)TypeId.Int32, nullable: false));
+        }
+
+        return new TypeMeta(
+            (uint)TypeId.Struct,
+            userTypeId,
+            MetaString.Empty('.', '_'),
+            MetaString.Empty('$', '_'),
+            registerByName: false,
+            fields);
+    }
+
+    private static TypeMeta RemoteNamedNonStructTypeMeta(TypeId typeId, string typeName)
+    {
+        return new TypeMeta(
+            (uint)typeId,
+            null,
+            MetaStringEncoder.Namespace.Encode("example", TypeMetaEncodings.NamespaceMetaStringEncodings),
+            MetaStringEncoder.TypeName.Encode(typeName, TypeMetaEncodings.TypeNameMetaStringEncodings),
+            registerByName: true,
+            []);
+    }
+
+    private static TypeMeta RemoteCompatibleStructTypeMeta(uint userTypeId, string fieldName)
+    {
+        return RemoteCompatibleStructTypeMeta(
+            userTypeId,
+            fieldName,
+            new TypeMetaFieldType((uint)TypeId.Int32, nullable: false));
+    }
+
+    private static TypeMeta RemoteCompatibleStructTypeMeta(
+        uint userTypeId,
+        string fieldName,
+        TypeMetaFieldType fieldType)
+    {
+        return new TypeMeta(
+            (uint)TypeId.CompatibleStruct,
+            userTypeId,
+            MetaString.Empty('.', '_'),
+            MetaString.Empty('$', '_'),
+            registerByName: false,
+            [new TypeMetaFieldInfo(null, fieldName, fieldType)]);
+    }
+
+    private static TypeMetaFieldType MapType()
+    {
+        return new TypeMetaFieldType(
+            (uint)TypeId.Map,
+            nullable: false,
+            trackRef: false,
+            [
+                new TypeMetaFieldType((uint)TypeId.String, nullable: false),
+                new TypeMetaFieldType((uint)TypeId.Int32, nullable: false),
+            ]);
+    }
+
+    private static TypeMeta ReadAndStoreTypeMeta(ReadContext context, TypeMeta typeMeta)
+    {
+        ByteWriter writer = new();
+        writer.WriteVarUInt32(0);
+        writer.WriteBytes(typeMeta.Encode());
+        context.ResetFor(new ByteReader(writer.ToArray()));
+        return context.ReadTypeMeta();
+    }
+
+    private static TypeInfo ReadAnyTypeInfo(ReadContext context, TypeResolver resolver, TypeMeta typeMeta)
+    {
+        ByteWriter writer = new();
+        writer.WriteUInt8((byte)TypeId.CompatibleStruct);
+        writer.WriteVarUInt32(0);
+        writer.WriteBytes(typeMeta.Encode());
+        context.ResetFor(new ByteReader(writer.ToArray()));
+        return resolver.ReadAnyTypeInfo(context);
+    }
+
+    private static ulong EncodedTypeMetaHeader(TypeMeta typeMeta)
+    {
+        return BitConverter.ToUInt64(typeMeta.Encode(), 0);
+    }
+
     [Fact]
     public void DynamicAnyRejectsUnknownUserTypeId()
     {
diff --git a/csharp/tests/Fory.Tests/StringSerializerTests.cs b/csharp/tests/Fory.Tests/StringSerializerTests.cs
index 1b9ca4bf8..f0e97e45d 100644
--- a/csharp/tests/Fory.Tests/StringSerializerTests.cs
+++ b/csharp/tests/Fory.Tests/StringSerializerTests.cs
@@ -74,7 +74,8 @@ public sealed class StringSerializerTests
         writer.WriteVarUInt36Small((3UL << 2) | Utf16);
         writer.WriteBytes([0x61, 0x00, 0x62]);
 
-        ReadContext context = new(new ByteReader(writer.ToArray()), new TypeResolver(), trackRef: false, compatible: false);
+        Config config = Fory.Builder().Compatible(false).Build().Config;
+        ReadContext context = new(new ByteReader(writer.ToArray()), new TypeResolver(), config);
         Assert.Throws<EncodingException>(() => StringSerializer.ReadString(context));
     }
 
@@ -84,7 +85,8 @@ public sealed class StringSerializerTests
         ByteWriter writer = new();
         writer.WriteVarUInt36Small(3UL);
 
-        ReadContext context = new(new ByteReader(writer.ToArray()), new TypeResolver(), trackRef: false, compatible: false);
+        Config config = Fory.Builder().Compatible(false).Build().Config;
+        ReadContext context = new(new ByteReader(writer.ToArray()), new TypeResolver(), config);
         Assert.Throws<EncodingException>(() => StringSerializer.ReadString(context));
     }
 
@@ -119,7 +121,8 @@ public sealed class StringSerializerTests
         int byteLength = checked((int)(header >> 2));
         Assert.Equal(payload.Length - headerReader.Cursor, byteLength);
 
-        ReadContext readContext = new(new ByteReader(payload), resolver, trackRef: false, compatible: false);
+        Config config = Fory.Builder().Compatible(false).Build().Config;
+        ReadContext readContext = new(new ByteReader(payload), resolver, config);
         string decoded = StringSerializer.ReadString(readContext);
         Assert.Equal(0, readContext.Reader.Remaining);
         return (encoding, decoded, headerReader.Cursor, byteLength);
diff --git a/dart/packages/fory-test/lib/entity/xlang_test_manual.dart b/dart/packages/fory-test/lib/entity/xlang_test_manual.dart
index f9a551f64..05ea5f6e8 100644
--- a/dart/packages/fory-test/lib/entity/xlang_test_manual.dart
+++ b/dart/packages/fory-test/lib/entity/xlang_test_manual.dart
@@ -196,7 +196,7 @@ final class _RefOverrideContainerForySerializer
       context.writeResolvedValue(field0Declared, field0Value, field0.fieldType);
     } else {
       final actualResolved = context.typeResolver.resolveValue(field0Value);
-      context.writeTypeMetaValue(actualResolved, field0Value);
+      context.writeTypeMetaValue(actualResolved);
       context.writeResolvedValue(actualResolved, field0Value, field0.fieldType);
     }
     final field1 = fields[1];
@@ -206,7 +206,7 @@ final class _RefOverrideContainerForySerializer
       context.writeResolvedValue(field1Declared, field1Value, field1.fieldType);
     } else {
       final actualResolved = context.typeResolver.resolveValue(field1Value);
-      context.writeTypeMetaValue(actualResolved, field1Value);
+      context.writeTypeMetaValue(actualResolved);
       context.writeResolvedValue(actualResolved, field1Value, field1.fieldType);
     }
     final field2 = fields[2];
@@ -216,7 +216,7 @@ final class _RefOverrideContainerForySerializer
       context.writeResolvedValue(field2Declared, field2Value, field2.fieldType);
     } else {
       final actualResolved = context.typeResolver.resolveValue(field2Value);
-      context.writeTypeMetaValue(actualResolved, field2Value);
+      context.writeTypeMetaValue(actualResolved);
       context.writeResolvedValue(actualResolved, field2Value, field2.fieldType);
     }
   }
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 debe8a827..d96850030 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
@@ -85,7 +85,7 @@ void _runIntegerCase() {
   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));
+    output.add(fory.serializeBuiltin(value, typeId: TypeIds.varInt32));
   }
 
   if (input.readableBytes != 0) {
diff --git a/dart/packages/fory/lib/src/codegen/fory_generator.dart b/dart/packages/fory/lib/src/codegen/fory_generator.dart
index 79e33a278..cae611048 100644
--- a/dart/packages/fory/lib/src/codegen/fory_generator.dart
+++ b/dart/packages/fory/lib/src/codegen/fory_generator.dart
@@ -2514,7 +2514,7 @@ GeneratedFieldType(
       ..writeln(
         '${indent}final actualResolved = context.typeResolver.resolveValue($value as Object);',
       )
-      ..writeln('${indent}context.writeTypeMetaValue(actualResolved, $value);')
+      ..writeln('${indent}context.writeTypeMetaValue(actualResolved);')
       ..writeln(
         '${indent}context.writeResolvedValue(actualResolved, $value, $fieldType);',
       );
diff --git a/dart/packages/fory/lib/src/codegen/generated_support.dart b/dart/packages/fory/lib/src/codegen/generated_support.dart
index b41c2fa1c..fd0c14d03 100644
--- a/dart/packages/fory/lib/src/codegen/generated_support.dart
+++ b/dart/packages/fory/lib/src/codegen/generated_support.dart
@@ -440,7 +440,7 @@ void writeGeneratedUnionCaseValue(
   )) {
     return;
   }
-  context.writeTypeMetaValue(resolved, value);
+  context.writeTypeMetaValue(resolved);
   context.writeResolvedValue(resolved, value, fieldType);
 }
 
diff --git a/dart/packages/fory/lib/src/config.dart b/dart/packages/fory/lib/src/config.dart
index 2ab848e57..d5d248cd3 100644
--- a/dart/packages/fory/lib/src/config.dart
+++ b/dart/packages/fory/lib/src/config.dart
@@ -24,6 +24,10 @@ final class Config {
   /// Default maximum nesting depth for a single serialization or
   /// deserialization operation.
   static const int defaultMaxDepth = 256;
+  static const int defaultMaxTypeFields = 512;
+  static const int defaultMaxTypeMetaBytes = 4096;
+  static const int defaultMaxSchemaVersionsPerType = 10;
+  static const int defaultMaxAverageSchemaVersionsPerType = 3;
 
   /// Enables compatible struct encoding and decoding.
   ///
@@ -39,6 +43,19 @@ final class Config {
   /// Maximum allowed read or write nesting depth.
   final int maxDepth;
 
+  /// Maximum accepted field count in one received struct TypeDef.
+  final int maxTypeFields;
+
+  /// Maximum accepted body size in one received TypeDef.
+  final int maxTypeMetaBytes;
+
+  /// Maximum accepted remote metadata versions for one logical type.
+  final int maxSchemaVersionsPerType;
+
+  /// Maximum accepted average remote metadata versions across logical
+  /// types.
+  final int maxAverageSchemaVersionsPerType;
+
   /// Creates an immutable configuration object.
   ///
   /// Invalid numeric limits fail fast. When [compatible] is `true`,
@@ -47,6 +64,21 @@ final class Config {
     this.compatible = true,
     bool checkStructVersion = true,
     this.maxDepth = defaultMaxDepth,
+    this.maxTypeFields = defaultMaxTypeFields,
+    this.maxTypeMetaBytes = defaultMaxTypeMetaBytes,
+    this.maxSchemaVersionsPerType = defaultMaxSchemaVersionsPerType,
+    this.maxAverageSchemaVersionsPerType =
+        defaultMaxAverageSchemaVersionsPerType,
   }) : checkStructVersion = compatible ? false : checkStructVersion,
-       assert(maxDepth > 0, 'maxDepth must be positive');
+       assert(maxDepth > 0, 'maxDepth must be positive'),
+       assert(maxTypeFields > 0, 'maxTypeFields must be positive'),
+       assert(maxTypeMetaBytes > 0, 'maxTypeMetaBytes must be positive'),
+       assert(
+         maxSchemaVersionsPerType > 0,
+         'maxSchemaVersionsPerType must be positive',
+       ),
+       assert(
+         maxAverageSchemaVersionsPerType > 0,
+         'maxAverageSchemaVersionsPerType must be positive',
+       );
 }
diff --git a/dart/packages/fory/lib/src/context/read_context.dart b/dart/packages/fory/lib/src/context/read_context.dart
index 57b3d0cf2..faa8191ab 100644
--- a/dart/packages/fory/lib/src/context/read_context.dart
+++ b/dart/packages/fory/lib/src/context/read_context.dart
@@ -220,15 +220,13 @@ final class ReadContext {
     final typeMetaResolved =
         expectedRootType == null
             ? _readTypeMeta()
-            : _typeResolver.readExpectedInitialTypeDefMeta(
+            : _typeResolver.readExpectedTypeDefMeta(
                   _buffer,
                   expectedRootType,
                   sharedTypes: _sharedTypes,
                 ) ??
                 _readTypeMeta(expectedRootType);
-    final resolved = _typeResolver.resolveExpectedRootWireType<T>(
-      typeMetaResolved,
-    );
+    final resolved = _typeResolver.resolveExpectedRootType<T>(typeMetaResolved);
     final rootPreservedRefId =
         preservedRefId == null &&
                 flag == RefWriter.notNullValueFlag &&
diff --git a/dart/packages/fory/lib/src/context/write_context.dart b/dart/packages/fory/lib/src/context/write_context.dart
index 4748995eb..4b92e7c25 100644
--- a/dart/packages/fory/lib/src/context/write_context.dart
+++ b/dart/packages/fory/lib/src/context/write_context.dart
@@ -172,7 +172,7 @@ final class WriteContext {
   /// Writes a non-null value together with its type metadata.
   void writeNonRef(Object value) {
     final resolved = _typeResolver.resolveValue(value);
-    _writeTypeMeta(resolved, value);
+    _writeTypeMeta(resolved);
     writeResolvedValue(resolved, value, null);
   }
 
@@ -206,17 +206,17 @@ final class WriteContext {
     if (resolved.needsRootRef) {
       _refWriter.reference(value);
     }
-    _writeTypeMeta(resolved, value);
+    _writeTypeMeta(resolved);
     writeResolvedValue(resolved, value, null);
   }
 
   @internal
   void writeRootBuiltinValue(
     Object value, {
-    required int wireTypeId,
+    required int typeId,
     required bool trackRef,
   }) {
-    final resolved = _typeResolver.resolveBuiltinWireType(wireTypeId);
+    final resolved = _typeResolver.resolveBuiltinTypeId(typeId);
     final effectiveTrackRef = trackRef && resolved.supportsRef;
     if (_refWriter.writeRefOrNull(
       _buffer,
@@ -228,7 +228,7 @@ final class WriteContext {
     if (!effectiveTrackRef && resolved.needsRootRef) {
       _refWriter.reference(value);
     }
-    _writeTypeMeta(resolved, value);
+    _writeTypeMeta(resolved);
     writeResolvedValue(resolved, value, null);
   }
 
@@ -246,7 +246,7 @@ final class WriteContext {
     if (value == null) {
       return;
     }
-    _writeTypeMeta(resolved!, value);
+    _writeTypeMeta(resolved!);
     writeResolvedValue(resolved, value, null);
   }
 
@@ -398,32 +398,18 @@ final class WriteContext {
     }
   }
 
-  void _writeTypeMeta(TypeInfo resolved, Object value) {
-    final typeDef = resolved.structSerializer?.typeDefForWrite(
-      this,
-      resolved,
-      value,
-    );
-    if (_typeResolver.writeInitialTypeDefMeta(
-      _buffer,
-      resolved,
-      typeDef: typeDef,
-      typeDefIds: _typeDefIds,
-    )) {
-      return;
-    }
+  void _writeTypeMeta(TypeInfo resolved) {
     _typeResolver.writeTypeMeta(
       _buffer,
       resolved,
-      typeDef: typeDef,
       typeDefIds: _typeDefIds,
       metaStringWriter: _metaStringWriter,
     );
   }
 
   @internal
-  void writeTypeMetaValue(TypeInfo resolved, Object value) {
-    _writeTypeMeta(resolved, value);
+  void writeTypeMetaValue(TypeInfo resolved) {
+    _writeTypeMeta(resolved);
   }
 
   bool _tracksDepth(TypeInfo resolved) {
diff --git a/dart/packages/fory/lib/src/fory.dart b/dart/packages/fory/lib/src/fory.dart
index 6daaa072e..adc6091a8 100644
--- a/dart/packages/fory/lib/src/fory.dart
+++ b/dart/packages/fory/lib/src/fory.dart
@@ -57,11 +57,20 @@ final class Fory {
     bool compatible = true,
     bool checkStructVersion = true,
     int maxDepth = Config.defaultMaxDepth,
+    int maxTypeFields = Config.defaultMaxTypeFields,
+    int maxTypeMetaBytes = Config.defaultMaxTypeMetaBytes,
+    int maxSchemaVersionsPerType = Config.defaultMaxSchemaVersionsPerType,
+    int maxAverageSchemaVersionsPerType =
+        Config.defaultMaxAverageSchemaVersionsPerType,
   }) {
     final config = Config(
       compatible: compatible,
       checkStructVersion: checkStructVersion,
       maxDepth: maxDepth,
+      maxTypeFields: maxTypeFields,
+      maxTypeMetaBytes: maxTypeMetaBytes,
+      maxSchemaVersionsPerType: maxSchemaVersionsPerType,
+      maxAverageSchemaVersionsPerType: maxAverageSchemaVersionsPerType,
     );
     _readBuffer = Buffer();
     _writeBuffer = Buffer();
@@ -91,24 +100,18 @@ final class Fory {
     return Uint8List.fromList(_writeBuffer.toBytes());
   }
 
-  /// Serializes a non-null builtin [value] using the explicit xlang
-  /// [wireTypeId].
+  /// Serializes a non-null builtin [value] using the explicit xlang [typeId].
   ///
-  /// This is the advanced escape hatch for root values whose wire type must
+  /// This is the advanced escape hatch for root values whose xlang 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.
+  /// as xlang `VAR_INT32`. [typeId] must identify a builtin xlang type.
   Uint8List serializeBuiltin(
     Object value, {
-    required int wireTypeId,
+    required int typeId,
     bool trackRef = false,
   }) {
     _writeBuffer.clear();
-    serializeBuiltinTo(
-      value,
-      _writeBuffer,
-      wireTypeId: wireTypeId,
-      trackRef: trackRef,
-    );
+    serializeBuiltinTo(value, _writeBuffer, typeId: typeId, trackRef: trackRef);
     return Uint8List.fromList(_writeBuffer.toBytes());
   }
 
@@ -132,14 +135,14 @@ final class Fory {
   }
 
   /// Serializes a non-null builtin [value] into [buffer] using the explicit
-  /// xlang [wireTypeId].
+  /// xlang [typeId].
   ///
-  /// The target [buffer] is cleared before bytes are written. [wireTypeId]
-  /// must identify a builtin xlang type.
+  /// The target [buffer] is cleared before bytes are written. [typeId] must
+  /// identify a builtin xlang type.
   void serializeBuiltinTo(
     Object value,
     Buffer buffer, {
-    required int wireTypeId,
+    required int typeId,
     bool trackRef = false,
   }) {
     buffer.clear();
@@ -148,7 +151,7 @@ final class Fory {
       buffer.writeUint8(_xlangHeaderFlag);
       _writeContext.writeRootBuiltinValue(
         value,
-        wireTypeId: wireTypeId,
+        typeId: typeId,
         trackRef: trackRef,
       );
     } finally {
diff --git a/dart/packages/fory/lib/src/memory/buffer_mixin.dart b/dart/packages/fory/lib/src/memory/buffer_mixin.dart
index 2c0206480..79c3ec331 100644
--- a/dart/packages/fory/lib/src/memory/buffer_mixin.dart
+++ b/dart/packages/fory/lib/src/memory/buffer_mixin.dart
@@ -333,6 +333,20 @@ void bufferSetReaderIndex(Buffer buffer, int index) {
   buffer._readerIndex = index;
 }
 
+@internal
+bool bufferMatchesBytes(Buffer buffer, int start, Uint8List bytes) {
+  final end = start + bytes.length;
+  if (start < 0 || end > buffer._writerIndex) {
+    return false;
+  }
+  for (var i = 0; i < bytes.length; i += 1) {
+    if (buffer._bytes[start + i] != bytes[i]) {
+      return false;
+    }
+  }
+  return true;
+}
+
 @internal
 void bufferWriteUint8At(Buffer buffer, int index, int value) {
   buffer._view.setUint8(index, value);
diff --git a/dart/packages/fory/lib/src/meta/type_meta.dart b/dart/packages/fory/lib/src/meta/type_meta.dart
index 7d77e1725..08546df33 100644
--- a/dart/packages/fory/lib/src/meta/type_meta.dart
+++ b/dart/packages/fory/lib/src/meta/type_meta.dart
@@ -28,30 +28,30 @@ import 'package:fory/src/resolver/type_resolver.dart';
 import 'package:fory/src/types/int64.dart';
 import 'package:fory/src/util/hash_util.dart';
 
-/// Wire-level type metadata for one value.
-final class WireTypeMeta {
+/// Type metadata for one serialized value.
+final class TypeMeta {
   final TypeInfo resolvedType;
-  final int wireTypeId;
-  final bool writesTypeDef;
+  final int typeId;
+  final bool hasTypeDef;
 
-  const WireTypeMeta({
+  const TypeMeta({
     required this.resolvedType,
-    required this.wireTypeId,
-    required this.writesTypeDef,
+    required this.typeId,
+    required this.hasTypeDef,
   });
 
-  bool get writesUserTypeId =>
-      wireTypeId == TypeIds.enumById ||
-      wireTypeId == TypeIds.struct ||
-      wireTypeId == TypeIds.ext ||
-      wireTypeId == TypeIds.typedUnion;
+  bool get hasUserTypeId =>
+      typeId == TypeIds.enumById ||
+      typeId == TypeIds.struct ||
+      typeId == TypeIds.ext ||
+      typeId == TypeIds.typedUnion;
 
-  bool get writesNamedType =>
-      !writesTypeDef &&
-      (wireTypeId == TypeIds.namedEnum ||
-          wireTypeId == TypeIds.namedStruct ||
-          wireTypeId == TypeIds.namedExt ||
-          wireTypeId == TypeIds.namedUnion);
+  bool get hasEncodedName =>
+      !hasTypeDef &&
+      (typeId == TypeIds.namedEnum ||
+          typeId == TypeIds.namedStruct ||
+          typeId == TypeIds.namedExt ||
+          typeId == TypeIds.namedUnion);
 }
 
 final class TypeHeader {
@@ -76,7 +76,12 @@ final class TypeHeader {
 
   @pragma('vm:prefer-inline')
   int readMetaSize(Buffer buffer) {
-    final lowBits = value.low32 & 0xff;
+    return readMetaSizeFromHeader(buffer, value);
+  }
+
+  @pragma('vm:prefer-inline')
+  static int readMetaSizeFromHeader(Buffer buffer, Int64 header) {
+    final lowBits = header.low32 & 0xff;
     if (lowBits == 0xff) {
       return 0xff + buffer.readVarUint32Small14();
     }
@@ -85,7 +90,12 @@ final class TypeHeader {
 
   @pragma('vm:prefer-inline')
   void skipRemaining(Buffer buffer) {
-    buffer.skip(readMetaSize(buffer));
+    skipBody(buffer, value);
+  }
+
+  @pragma('vm:prefer-inline')
+  static void skipBody(Buffer buffer, Int64 header) {
+    buffer.skip(readMetaSizeFromHeader(buffer, header));
   }
 
   @pragma('vm:prefer-inline')
@@ -102,153 +112,73 @@ final class TypeHeader {
 }
 
 final class ParsedTypeMetaCache {
-  static const int maxEntries = 8192;
-
   final LinkedHashMap<Int64, TypeInfo> _entries =
       LinkedHashMap<Int64, TypeInfo>();
-  Int64? _lastHeader;
-  TypeInfo? _lastResolved;
+  TypeInfo? _cachedTypeInfo;
 
   @pragma('vm:prefer-inline')
   TypeInfo? lookup(TypeHeader header) {
-    if (_lastHeader == header.value) {
-      return _lastResolved;
+    // The hot hint is one TypeInfo object. The validated TypeDef header lives
+    // on that metadata object; do not add parallel header slots, sentinel
+    // fields, accepted-header state, or body/hash/schema checks to this path.
+    final cached = _cachedTypeInfo;
+    if (cached != null && cached.cachedTypeDefHeader == header.value) {
+      return cached;
     }
     final resolved = _entries[header.value];
     if (resolved != null) {
-      _lastHeader = header.value;
-      _lastResolved = resolved;
+      _cachedTypeInfo = resolved;
     }
     return resolved;
   }
 
   @pragma('vm:prefer-inline')
   void remember(TypeHeader header, TypeInfo resolved) {
-    final cached = _entries[header.value];
-    if (cached != null) {
-      _entries[header.value] = resolved;
-      _lastHeader = header.value;
-      _lastResolved = resolved;
-      return;
-    }
-    if (_entries.length >= maxEntries) {
-      return;
-    }
-
+    assert(resolved.cachedTypeDefHeader == header.value);
     _entries[header.value] = resolved;
-    _lastHeader = header.value;
-    _lastResolved = resolved;
-  }
-}
-
-/// Encodes type metadata into the xlang wire format.
-final class WireTypeMetaEncoder {
-  const WireTypeMetaEncoder();
-
-  WireTypeMeta typeMetaFor(Config config, TypeInfo resolvedType) {
-    final wireTypeId = wireTypeIdFor(config, resolvedType);
-    final writesTypeDef = wireTypeId == TypeIds.compatibleStruct ||
-        wireTypeId == TypeIds.namedCompatibleStruct ||
-        (config.compatible &&
-            (wireTypeId == TypeIds.namedEnum ||
-                wireTypeId == TypeIds.namedStruct ||
-                wireTypeId == TypeIds.namedExt ||
-                wireTypeId == TypeIds.namedUnion));
-    return WireTypeMeta(
-      resolvedType: resolvedType,
-      wireTypeId: wireTypeId,
-      writesTypeDef: writesTypeDef,
-    );
-  }
-
-  void write(
-    Buffer buffer,
-    WireTypeMeta typeMeta, {
-    required void Function(WireTypeMeta typeMeta) writeTypeDef,
-    required void Function(EncodedMetaString value) writePackageMetaString,
-    required void Function(EncodedMetaString value) writeTypeNameMetaString,
-  }) {
-    buffer.writeVarUint32Small7(typeMeta.wireTypeId);
-    if (typeMeta.writesUserTypeId) {
-      buffer.writeVarUint32(typeMeta.resolvedType.userTypeId!);
-      return;
-    }
-    if (typeMeta.writesTypeDef) {
-      writeTypeDef(typeMeta);
-      return;
-    }
-    if (typeMeta.writesNamedType) {
-      writePackageMetaString(typeMeta.resolvedType.encodedNamespace!);
-      writeTypeNameMetaString(typeMeta.resolvedType.encodedTypeName!);
-    }
-  }
-
-  @pragma('vm:prefer-inline')
-  int wireTypeIdFor(Config config, TypeInfo resolvedType) {
-    switch (resolvedType.kind) {
-      case RegistrationKind.builtin:
-        return resolvedType.typeId;
-      case RegistrationKind.enumType:
-        return resolvedType.isNamed ? TypeIds.namedEnum : TypeIds.enumById;
-      case RegistrationKind.ext:
-        return resolvedType.isNamed ? TypeIds.namedExt : TypeIds.ext;
-      case RegistrationKind.union:
-        if (resolvedType.userTypeId != null) {
-          return TypeIds.typedUnion;
-        }
-        return resolvedType.isNamed ? TypeIds.namedUnion : TypeIds.union;
-      case RegistrationKind.struct:
-        final compatible = config.compatible && resolvedType.typeDef!.evolving;
-        if (compatible) {
-          return resolvedType.isNamed
-              ? TypeIds.namedCompatibleStruct
-              : TypeIds.compatibleStruct;
-        }
-        return resolvedType.isNamed ? TypeIds.namedStruct : TypeIds.struct;
-    }
+    _cachedTypeInfo = resolved;
   }
 }
 
 /// Decodes type metadata from the xlang wire format.
-final class WireTypeMetaDecoder {
-  const WireTypeMetaDecoder();
+final class TypeMetaDecoder {
+  const TypeMetaDecoder();
 
-  WireTypeMeta read(
+  TypeMeta read(
     Buffer buffer, {
     required Config config,
-    required TypeInfo Function(int wireTypeId) resolveBuiltinWireType,
+    required TypeInfo Function(int typeId) resolveBuiltinTypeId,
     required TypeInfo Function(int id) resolveUserById,
     required TypeInfo Function(
-      int wireTypeId,
+      int typeId,
       EncodedMetaString namespace,
       EncodedMetaString typeName,
-    ) resolveUserByEncodedNameCached,
-    required TypeInfo? Function(int wireTypeId) expectedNamedType,
-    required WireTypeMeta Function() readTypeDef,
-    required EncodedMetaString Function([
-      EncodedMetaString? expected,
-    ]) readPackageMetaString,
-    required EncodedMetaString Function([
-      EncodedMetaString? expected,
-    ]) readTypeNameMetaString,
+    )
+    resolveUserByEncodedNameCached,
+    required TypeInfo? Function(int typeId) expectedNamedType,
+    required TypeMeta Function() readTypeDef,
+    required EncodedMetaString Function([EncodedMetaString? expected])
+    readPackageMetaString,
+    required EncodedMetaString Function([EncodedMetaString? expected])
+    readTypeNameMetaString,
   }) {
-    final wireTypeId = buffer.readVarUint32Small7();
-    if (_isBuiltinWireType(wireTypeId)) {
-      return WireTypeMeta(
-        resolvedType: resolveBuiltinWireType(wireTypeId),
-        wireTypeId: wireTypeId,
-        writesTypeDef: false,
+    final typeId = buffer.readVarUint32Small7();
+    if (_isBuiltinTypeId(typeId)) {
+      return TypeMeta(
+        resolvedType: resolveBuiltinTypeId(typeId),
+        typeId: typeId,
+        hasTypeDef: false,
       );
     }
-    switch (wireTypeId) {
+    switch (typeId) {
       case TypeIds.enumById:
       case TypeIds.struct:
       case TypeIds.ext:
       case TypeIds.typedUnion:
-        return WireTypeMeta(
+        return TypeMeta(
           resolvedType: resolveUserById(buffer.readVarUint32()),
-          wireTypeId: wireTypeId,
-          writesTypeDef: false,
+          typeId: typeId,
+          hasTypeDef: false,
         );
       case TypeIds.namedEnum:
       case TypeIds.namedStruct:
@@ -257,76 +187,76 @@ final class WireTypeMetaDecoder {
         if (config.compatible) {
           return readTypeDef();
         }
-        final expected = expectedNamedType(wireTypeId);
+        final expected = expectedNamedType(typeId);
         final namespace = readPackageMetaString(expected?.encodedNamespace);
         final typeName = readTypeNameMetaString(expected?.encodedTypeName);
         if (expected != null &&
             identical(namespace, expected.encodedNamespace) &&
             identical(typeName, expected.encodedTypeName)) {
-          return WireTypeMeta(
+          return TypeMeta(
             resolvedType: expected,
-            wireTypeId: wireTypeId,
-            writesTypeDef: false,
+            typeId: typeId,
+            hasTypeDef: false,
           );
         }
-        return WireTypeMeta(
+        return TypeMeta(
           resolvedType: resolveUserByEncodedNameCached(
-            wireTypeId,
+            typeId,
             namespace,
             typeName,
           ),
-          wireTypeId: wireTypeId,
-          writesTypeDef: false,
+          typeId: typeId,
+          hasTypeDef: false,
         );
       case TypeIds.compatibleStruct:
       case TypeIds.namedCompatibleStruct:
         return readTypeDef();
       default:
-        throw StateError('Unsupported wire type id $wireTypeId.');
+        throw StateError('Unsupported type id $typeId.');
     }
   }
 
-  bool _isBuiltinWireType(int wireTypeId) =>
-      wireTypeId == TypeIds.boolType ||
-      wireTypeId == TypeIds.int8 ||
-      wireTypeId == TypeIds.int16 ||
-      wireTypeId == TypeIds.int32 ||
-      wireTypeId == TypeIds.varInt32 ||
-      wireTypeId == TypeIds.int64 ||
-      wireTypeId == TypeIds.varInt64 ||
-      wireTypeId == TypeIds.taggedInt64 ||
-      wireTypeId == TypeIds.uint8 ||
-      wireTypeId == TypeIds.uint16 ||
-      wireTypeId == TypeIds.uint32 ||
-      wireTypeId == TypeIds.varUint32 ||
-      wireTypeId == TypeIds.uint64 ||
-      wireTypeId == TypeIds.varUint64 ||
-      wireTypeId == TypeIds.taggedUint64 ||
-      wireTypeId == TypeIds.float16 ||
-      wireTypeId == TypeIds.bfloat16 ||
-      wireTypeId == TypeIds.float32 ||
-      wireTypeId == TypeIds.float64 ||
-      wireTypeId == TypeIds.string ||
-      wireTypeId == TypeIds.list ||
-      wireTypeId == TypeIds.set ||
-      wireTypeId == TypeIds.map ||
-      wireTypeId == TypeIds.none ||
-      wireTypeId == TypeIds.binary ||
-      wireTypeId == TypeIds.duration ||
-      wireTypeId == TypeIds.decimal ||
-      wireTypeId == TypeIds.date ||
-      wireTypeId == TypeIds.timestamp ||
-      wireTypeId == TypeIds.boolArray ||
-      wireTypeId == TypeIds.int8Array ||
-      wireTypeId == TypeIds.int16Array ||
-      wireTypeId == TypeIds.int32Array ||
-      wireTypeId == TypeIds.int64Array ||
-      wireTypeId == TypeIds.uint8Array ||
-      wireTypeId == TypeIds.uint16Array ||
-      wireTypeId == TypeIds.uint32Array ||
-      wireTypeId == TypeIds.uint64Array ||
-      wireTypeId == TypeIds.float16Array ||
-      wireTypeId == TypeIds.bfloat16Array ||
-      wireTypeId == TypeIds.float32Array ||
-      wireTypeId == TypeIds.float64Array;
+  bool _isBuiltinTypeId(int typeId) =>
+      typeId == TypeIds.boolType ||
+      typeId == TypeIds.int8 ||
+      typeId == TypeIds.int16 ||
+      typeId == TypeIds.int32 ||
+      typeId == TypeIds.varInt32 ||
+      typeId == TypeIds.int64 ||
+      typeId == TypeIds.varInt64 ||
+      typeId == TypeIds.taggedInt64 ||
+      typeId == TypeIds.uint8 ||
+      typeId == TypeIds.uint16 ||
+      typeId == TypeIds.uint32 ||
+      typeId == TypeIds.varUint32 ||
+      typeId == TypeIds.uint64 ||
+      typeId == TypeIds.varUint64 ||
+      typeId == TypeIds.taggedUint64 ||
+      typeId == TypeIds.float16 ||
+      typeId == TypeIds.bfloat16 ||
+      typeId == TypeIds.float32 ||
+      typeId == TypeIds.float64 ||
+      typeId == TypeIds.string ||
+      typeId == TypeIds.list ||
+      typeId == TypeIds.set ||
+      typeId == TypeIds.map ||
+      typeId == TypeIds.none ||
+      typeId == TypeIds.binary ||
+      typeId == TypeIds.duration ||
+      typeId == TypeIds.decimal ||
+      typeId == TypeIds.date ||
+      typeId == TypeIds.timestamp ||
+      typeId == TypeIds.boolArray ||
+      typeId == TypeIds.int8Array ||
+      typeId == TypeIds.int16Array ||
+      typeId == TypeIds.int32Array ||
+      typeId == TypeIds.int64Array ||
+      typeId == TypeIds.uint8Array ||
+      typeId == TypeIds.uint16Array ||
+      typeId == TypeIds.uint32Array ||
+      typeId == TypeIds.uint64Array ||
+      typeId == TypeIds.float16Array ||
+      typeId == TypeIds.bfloat16Array ||
+      typeId == TypeIds.float32Array ||
+      typeId == TypeIds.float64Array;
 }
diff --git a/dart/packages/fory/lib/src/resolver/type_resolver.dart b/dart/packages/fory/lib/src/resolver/type_resolver.dart
index 3ad648631..23a99c0fe 100644
--- a/dart/packages/fory/lib/src/resolver/type_resolver.dart
+++ b/dart/packages/fory/lib/src/resolver/type_resolver.dart
@@ -113,23 +113,27 @@ final class TypeInfo {
   final bool supportsRef;
   final bool needsRootRef;
   final bool usesNestedTypeDefinitions;
+  final bool evolving;
+  final List<FieldInfo> fields;
   final Serializer<Object?> serializer;
-  final StructSerializer? structSerializer;
+  StructSerializer? structSerializer;
   final int? userTypeId;
   final String? namespace;
   final String? typeName;
   final EncodedMetaString? encodedNamespace;
   final EncodedMetaString? encodedTypeName;
-  final TypeDef? typeDef;
+  TypeDef? typeDef;
   final TypeDef? remoteTypeDef;
 
-  const TypeInfo({
+  TypeInfo({
     required this.type,
     required this.kind,
     required this.typeId,
     required this.supportsRef,
     required this.needsRootRef,
     required this.usesNestedTypeDefinitions,
+    required this.evolving,
+    required this.fields,
     required this.serializer,
     required this.structSerializer,
     required this.userTypeId,
@@ -144,10 +148,11 @@ final class TypeInfo {
   bool get isNamed =>
       userTypeId == null && namespace != null && typeName != null;
 
-  bool get isCompatibleStruct =>
-      kind == RegistrationKind.struct && typeDef!.evolving;
+  bool get isCompatibleStruct => kind == RegistrationKind.struct && evolving;
 
   bool get isBasicValue => TypeIds.isBasicValue(typeId);
+
+  Int64 get cachedTypeDefHeader => remoteTypeDef?.header ?? typeDef!.header;
 }
 
 bool usesDeclaredTypeInfo(
@@ -242,14 +247,12 @@ List<FieldInfo> _validateFieldInfos(Iterable<FieldInfo> fields) {
 }
 
 final class TypeResolver {
+  static const int _minRemoteTypeMetaLimit = 8192;
+
   final Config config;
-  final WireTypeMetaEncoder _wireTypeMetaEncoder = const WireTypeMetaEncoder();
-  final WireTypeMetaDecoder _wireTypeMetaDecoder = const WireTypeMetaDecoder();
+  final TypeMetaDecoder _typeMetaDecoder = const TypeMetaDecoder();
   final ParsedTypeMetaCache _parsedTypeMetaCache = ParsedTypeMetaCache();
-  final List<TypeInfo?> _lastNamedTypeByWireType = List<TypeInfo?>.filled(
-    64,
-    null,
-  );
+  final List<TypeInfo?> _lastNamedTypeById = List<TypeInfo?>.filled(64, null);
   final Map<_BuiltinKey, TypeInfo> _builtinByKey = <_BuiltinKey, TypeInfo>{};
   final List<_NamedTypeReadCacheEntry?> _namedTypeLookupCache =
       List<_NamedTypeReadCacheEntry?>.filled(128, null);
@@ -271,8 +274,8 @@ final class TypeResolver {
       <String, EncodedMetaString>{};
   final Map<_EncodedMetaStringKey, EncodedMetaString>
   _internedEncodedMetaStrings = <_EncodedMetaStringKey, EncodedMetaString>{};
-  final Map<TypeInfo, Uint8List> _initialTypeMetaBytes =
-      LinkedHashMap<TypeInfo, Uint8List>.identity();
+  final Map<String, int> _remoteSchemaVersionsByType = <String, int>{};
+  int _totalAcceptedSchemaVersions = 0;
 
   TypeResolver(this.config);
 
@@ -351,18 +354,6 @@ final class TypeResolver {
         registrationKind == RegistrationKind.struct
             ? _validateFieldInfos(fields)
             : const <FieldInfo>[];
-    final typeDef = _buildTypeDef(
-      kind: registrationKind,
-      evolving: registrationKind == RegistrationKind.struct ? evolving : false,
-      userTypeId: id,
-      encodedNamespace: encodedNamespace,
-      encodedTypeName: encodedTypeName,
-      fields: normalizedFields,
-    );
-    final structSerializer =
-        registrationKind != RegistrationKind.struct
-            ? null
-            : StructSerializer(payloadSerializer, typeDef, this);
     final resolved = TypeInfo(
       type: type,
       kind: registrationKind,
@@ -377,17 +368,18 @@ final class TypeResolver {
               ? usesNestedTypeDefinitions ??
                   _fieldsUseNestedTypeDefinitions(normalizedFields)
               : true,
+      evolving: registrationKind == RegistrationKind.struct ? evolving : false,
+      fields: normalizedFields,
       serializer: payloadSerializer,
-      structSerializer: structSerializer,
+      structSerializer: null,
       userTypeId: id,
       namespace: resolvedNamespace,
       typeName: resolvedTypeName,
       encodedNamespace: encodedNamespace,
       encodedTypeName: encodedTypeName,
-      typeDef: typeDef,
+      typeDef: null,
       remoteTypeDef: null,
     );
-    _parsedTypeMetaCache.remember(TypeHeader(typeDef.header), resolved);
     _rememberResolved(
       type,
       resolved,
@@ -395,6 +387,37 @@ final class TypeResolver {
       namespace: resolvedNamespace,
       typeName: resolvedTypeName,
     );
+    _rebuildRegisteredTypeDefs();
+  }
+
+  void _rebuildRegisteredTypeDefs() {
+    // Registration is two-stage: first record every TypeInfo, then rebuild
+    // local TypeDefs from the complete current registry. A later enum/ext/union
+    // registration can change a struct field's wire kind, so rebuild all
+    // registered TypeDefs on the registration cold path instead of constructing
+    // them lazily from read/write hot paths.
+    final seen = Set<TypeInfo>.identity();
+    for (final resolved in _registeredByType.values) {
+      if (!seen.add(resolved)) {
+        continue;
+      }
+      final typeDef = _buildTypeDef(
+        kind: resolved.kind,
+        evolving: resolved.evolving,
+        userTypeId: resolved.userTypeId,
+        encodedNamespace: resolved.encodedNamespace,
+        encodedTypeName: resolved.encodedTypeName,
+        fields: resolved.fields,
+      );
+      resolved.typeDef = typeDef;
+      if (resolved.kind == RegistrationKind.struct) {
+        resolved.structSerializer = StructSerializer(
+          resolved.serializer,
+          typeDef,
+          this,
+        );
+      }
+    }
   }
 
   EncodedMetaString packageMetaString(String value) {
@@ -547,7 +570,6 @@ final class TypeResolver {
 
   TypeInfo? tryResolveFieldType(FieldType fieldType) {
     switch (fieldType.typeId) {
-      case TypeIds.unknown:
       case TypeIds.boolType:
       case TypeIds.int8:
       case TypeIds.int16:
@@ -645,29 +667,29 @@ final class TypeResolver {
   }
 
   TypeInfo resolveUserByEncodedNameCached(
-    int wireTypeId,
+    int typeId,
     EncodedMetaString namespace,
     EncodedMetaString typeName,
   ) {
     final slot =
-        _namedTypeLookupCacheIndex(wireTypeId, namespace, typeName) &
+        _namedTypeLookupCacheIndex(typeId, namespace, typeName) &
         (_namedTypeLookupCache.length - 1);
     final cached = _namedTypeLookupCache[slot];
     if (cached != null &&
-        cached.wireTypeId == wireTypeId &&
+        cached.typeId == typeId &&
         identical(cached.namespace, namespace) &&
         identical(cached.typeName, typeName)) {
       return cached.resolved;
     }
     final resolved = resolveUserByEncodedName(namespace, typeName);
     _namedTypeLookupCache[slot] = _NamedTypeReadCacheEntry(
-      wireTypeId,
+      typeId,
       namespace,
       typeName,
       resolved,
     );
-    if (wireTypeId < _lastNamedTypeByWireType.length) {
-      _lastNamedTypeByWireType[wireTypeId] = resolved;
+    if (typeId < _lastNamedTypeById.length) {
+      _lastNamedTypeById[typeId] = resolved;
     }
     return resolved;
   }
@@ -701,18 +723,26 @@ final class TypeResolver {
     );
   }
 
-  WireTypeMeta wireTypeMetaForResolved(TypeInfo resolved) {
-    return _wireTypeMetaEncoder.typeMetaFor(config, resolved);
+  TypeMeta typeMetaForResolved(TypeInfo resolved) {
+    final typeId = _typeIdFor(resolved);
+    return TypeMeta(
+      resolvedType: resolved,
+      typeId: typeId,
+      hasTypeDef: _hasTypeDef(typeId),
+    );
   }
 
   TypeDef typeDefForResolved(TypeInfo resolved, {List<FieldInfo>? fields}) {
-    final resolvedFields = resolved.typeDef?.fields;
-    if (fields == null || identical(fields, resolvedFields)) {
-      return resolved.typeDef!;
+    final localTypeDef = resolved.typeDef;
+    if (localTypeDef == null) {
+      throw StateError('Type ${resolved.type} does not have TypeDef metadata.');
+    }
+    if (fields == null || identical(fields, localTypeDef.fields)) {
+      return localTypeDef;
     }
     return _buildTypeDef(
       kind: resolved.kind,
-      evolving: resolved.typeDef!.evolving,
+      evolving: resolved.evolving,
       userTypeId: resolved.userTypeId,
       encodedNamespace: resolved.encodedNamespace,
       encodedTypeName: resolved.encodedTypeName,
@@ -723,123 +753,75 @@ final class TypeResolver {
   void writeTypeMeta(
     Buffer buffer,
     TypeInfo resolved, {
-    required TypeDef? typeDef,
     required LinkedHashMap<TypeDef, int> typeDefIds,
     required MetaStringWriter metaStringWriter,
   }) {
-    final wireTypeId = _wireTypeMetaEncoder.wireTypeIdFor(config, resolved);
-    buffer.writeVarUint32Small7(wireTypeId);
-    if (_wireTypeWritesUserTypeId(wireTypeId)) {
-      buffer.writeVarUint32(resolved.userTypeId!);
+    final typeId = _typeIdFor(resolved);
+    if (_hasTypeDef(typeId)) {
+      buffer.writeVarUint32Small7(typeId);
+      _writeTypeDef(buffer, resolved.typeDef!, typeDefIds: typeDefIds);
       return;
     }
-    if (_wireTypeWritesTypeDef(wireTypeId)) {
-      _writeTypeDef(
-        buffer,
-        typeDef ?? resolved.typeDef!,
-        typeDefIds: typeDefIds,
-      );
+    if (_hasUserTypeId(typeId)) {
+      buffer.writeVarUint32Small7(typeId);
+      buffer.writeVarUint32(resolved.userTypeId!);
       return;
     }
-    if (_wireTypeWritesNamedType(wireTypeId)) {
+    buffer.writeVarUint32Small7(typeId);
+    if (_hasEncodedName(typeId)) {
       metaStringWriter.writeMetaString(buffer, resolved.encodedNamespace!);
       metaStringWriter.writeMetaString(buffer, resolved.encodedTypeName!);
     }
   }
 
-  bool writeInitialTypeDefMeta(
-    Buffer buffer,
-    TypeInfo resolved, {
-    required TypeDef? typeDef,
-    required LinkedHashMap<TypeDef, int> typeDefIds,
-  }) {
-    final resolvedTypeDef = resolved.typeDef;
-    if (typeDefIds.isNotEmpty ||
-        resolvedTypeDef == null ||
-        !identical(typeDef ?? resolvedTypeDef, resolvedTypeDef)) {
-      return false;
-    }
-    final wireTypeId = _wireTypeMetaEncoder.wireTypeIdFor(config, resolved);
-    if (!_wireTypeWritesTypeDef(wireTypeId)) {
-      return false;
-    }
-    final bytes = _initialTypeMetaBytes.putIfAbsent(
-      resolved,
-      () => _encodeInitialTypeDefMeta(wireTypeId, resolvedTypeDef),
-    );
-    buffer.writeBytes(bytes);
-    if (resolved.usesNestedTypeDefinitions) {
-      typeDefIds[resolvedTypeDef] = 0;
-    }
-    return true;
-  }
-
-  TypeInfo? readExpectedInitialTypeDefMeta(
-    Buffer buffer,
-    TypeInfo expected, {
-    required List<TypeInfo> sharedTypes,
-  }) {
-    final start = bufferReaderIndex(buffer);
-    final wireTypeId = buffer.readVarUint32Small7();
-    if (wireTypeId != _wireTypeMetaEncoder.wireTypeIdFor(config, expected) ||
-        !_wireTypeWritesTypeDef(wireTypeId)) {
-      bufferSetReaderIndex(buffer, start);
-      return null;
-    }
-    final marker = buffer.readVarUint32Small14();
-    if ((marker & 1) == 1) {
-      return sharedTypes[marker >>> 1];
-    }
-    final header = TypeHeader(buffer.readInt64());
-    final expectedTypeDef = expected.typeDef;
-    if (expectedTypeDef == null || expectedTypeDef.header != header.value) {
-      final cached = _parsedTypeMetaCache.lookup(header);
-      if (cached != null) {
-        header.skipRemaining(buffer);
-        sharedTypes.add(cached);
-        return cached;
-      }
-      final resolved = _readTypeDefWithHeader(buffer, header);
-      _parsedTypeMetaCache.remember(header, resolved);
-      sharedTypes.add(resolved);
-      return resolved;
-    }
-    header.skipRemaining(buffer);
-    sharedTypes.add(expected);
-    return expected;
-  }
-
-  Uint8List _encodeInitialTypeDefMeta(int wireTypeId, TypeDef typeDef) {
-    final buffer = Buffer(typeDef.encoded.length + 2);
-    buffer.writeVarUint32Small7(wireTypeId);
-    buffer.writeVarUint32(0);
-    buffer.writeBytes(typeDef.encoded);
-    return buffer.toBytes();
-  }
-
   @pragma('vm:prefer-inline')
-  bool _wireTypeWritesUserTypeId(int wireTypeId) =>
-      wireTypeId == TypeIds.enumById ||
-      wireTypeId == TypeIds.struct ||
-      wireTypeId == TypeIds.ext ||
-      wireTypeId == TypeIds.typedUnion;
+  bool _hasUserTypeId(int typeId) =>
+      typeId == TypeIds.enumById ||
+      typeId == TypeIds.struct ||
+      typeId == TypeIds.ext ||
+      typeId == TypeIds.typedUnion;
 
   @pragma('vm:prefer-inline')
-  bool _wireTypeWritesTypeDef(int wireTypeId) =>
-      wireTypeId == TypeIds.compatibleStruct ||
-      wireTypeId == TypeIds.namedCompatibleStruct ||
+  bool _hasTypeDef(int typeId) =>
+      typeId == TypeIds.compatibleStruct ||
+      typeId == TypeIds.namedCompatibleStruct ||
       (config.compatible &&
-          (wireTypeId == TypeIds.namedEnum ||
-              wireTypeId == TypeIds.namedStruct ||
-              wireTypeId == TypeIds.namedExt ||
-              wireTypeId == TypeIds.namedUnion));
+          (typeId == TypeIds.namedEnum ||
+              typeId == TypeIds.namedStruct ||
+              typeId == TypeIds.namedExt ||
+              typeId == TypeIds.namedUnion));
 
   @pragma('vm:prefer-inline')
-  bool _wireTypeWritesNamedType(int wireTypeId) =>
-      wireTypeId == TypeIds.namedEnum ||
-      wireTypeId == TypeIds.namedStruct ||
-      wireTypeId == TypeIds.namedExt ||
-      wireTypeId == TypeIds.namedUnion;
+  bool _hasEncodedName(int typeId) =>
+      typeId == TypeIds.namedEnum ||
+      typeId == TypeIds.namedStruct ||
+      typeId == TypeIds.namedExt ||
+      typeId == TypeIds.namedUnion;
+
+  @pragma('vm:prefer-inline')
+  int _typeIdFor(TypeInfo resolved) {
+    switch (resolved.kind) {
+      case RegistrationKind.builtin:
+        return resolved.typeId;
+      case RegistrationKind.enumType:
+        return resolved.isNamed ? TypeIds.namedEnum : TypeIds.enumById;
+      case RegistrationKind.ext:
+        return resolved.isNamed ? TypeIds.namedExt : TypeIds.ext;
+      case RegistrationKind.union:
+        if (resolved.userTypeId != null) {
+          return TypeIds.typedUnion;
+        }
+        return resolved.isNamed ? TypeIds.namedUnion : TypeIds.union;
+      case RegistrationKind.struct:
+        final compatibleStruct = config.compatible && resolved.evolving;
+        if (compatibleStruct) {
+          return resolved.isNamed
+              ? TypeIds.namedCompatibleStruct
+              : TypeIds.compatibleStruct;
+        }
+        return resolved.isNamed ? TypeIds.namedStruct : TypeIds.struct;
+    }
+  }
 
   @pragma('vm:prefer-inline')
   TypeInfo readTypeMeta(
@@ -848,38 +830,84 @@ final class TypeResolver {
     required List<TypeInfo> sharedTypes,
     required MetaStringReader metaStringReader,
   }) {
-    final typeMeta = _wireTypeMetaDecoder.read(
+    final expected = expectedNamedType;
+    if (expected != null && _hasTypeDef(_typeIdFor(expected))) {
+      final resolved = readExpectedTypeDefMeta(
+        buffer,
+        expected,
+        sharedTypes: sharedTypes,
+      );
+      if (resolved != null) {
+        return resolved;
+      }
+    }
+    final typeMeta = _typeMetaDecoder.read(
       buffer,
       config: config,
-      resolveBuiltinWireType: resolveBuiltinWireType,
+      resolveBuiltinTypeId: resolveBuiltinTypeId,
       resolveUserById: resolveUserById,
       resolveUserByEncodedNameCached: resolveUserByEncodedNameCached,
-      expectedNamedType: (wireTypeId) {
+      expectedNamedType: (typeId) {
         final expected = expectedNamedType;
-        if (expected != null && _matchesNamedWireType(expected, wireTypeId)) {
+        if (expected != null && _matchesNamedTypeId(expected, typeId)) {
           return expected;
         }
-        return wireTypeId < _lastNamedTypeByWireType.length
-            ? _lastNamedTypeByWireType[wireTypeId]
+        return typeId < _lastNamedTypeById.length
+            ? _lastNamedTypeById[typeId]
             : null;
       },
-      readTypeDef:
-          () => _readTypeDef(
-            buffer,
-            sharedTypes: sharedTypes,
-            expectedType: expectedNamedType,
-          ),
+      readTypeDef: () => _readTypeDef(buffer, sharedTypes: sharedTypes),
       readPackageMetaString:
           ([expected]) => metaStringReader.readMetaString(buffer, expected),
       readTypeNameMetaString:
           ([expected]) => metaStringReader.readMetaString(buffer, expected),
     );
-    if (typeMeta.writesNamedType) {
-      _rememberNamedType(typeMeta.wireTypeId, typeMeta.resolvedType);
+    if (typeMeta.hasEncodedName) {
+      _rememberNamedType(typeMeta.typeId, typeMeta.resolvedType);
     }
     return typeMeta.resolvedType;
   }
 
+  TypeInfo? readExpectedTypeDefMeta(
+    Buffer buffer,
+    TypeInfo expected, {
+    required List<TypeInfo> sharedTypes,
+  }) {
+    final start = bufferReaderIndex(buffer);
+    final typeId = buffer.readVarUint32Small7();
+    if (typeId != _typeIdFor(expected) || !_hasTypeDef(typeId)) {
+      bufferSetReaderIndex(buffer, start);
+      return null;
+    }
+    final marker = buffer.readVarUint32Small14();
+    if ((marker & 1) == 1) {
+      return sharedTypes[marker >>> 1];
+    }
+    final headerValue = buffer.readInt64();
+    final expectedTypeDef = expected.typeDef;
+    if (expectedTypeDef != null && expectedTypeDef.header == headerValue) {
+      // The expected local TypeInfo owns this TypeDef header. This is a direct
+      // local-schema hit: skip the remote body, do not rehash, and do not
+      // publish to ParsedTypeMetaCache because no remote metadata miss occurred.
+      TypeHeader.skipBody(buffer, headerValue);
+      sharedTypes.add(expected);
+      return expected;
+    }
+    final header = TypeHeader(headerValue);
+    final cached = _parsedTypeMetaCache.lookup(header);
+    if (cached != null) {
+      // Header-cache hits intentionally skip without rehashing. Entries reach this cache only
+      // after a successful TypeDef parse and 52-bit metadata-hash validation. Do not add
+      // body/hash/schema-limit/exact-local checks here; the miss path owns them before publish.
+      header.skipRemaining(buffer);
+      sharedTypes.add(cached);
+      return cached;
+    }
+    final resolved = _readTypeDefWithHeader(buffer, header);
+    sharedTypes.add(resolved);
+    return resolved;
+  }
+
   void _writeTypeDef(
     Buffer buffer,
     TypeDef typeDef, {
@@ -1086,6 +1114,9 @@ final class TypeResolver {
   }
 
   int _typeDefFieldTypeId(FieldType fieldType) {
+    if (fieldType.typeId == TypeIds.unknown) {
+      return TypeIds.unknown;
+    }
     if (TypeIds.isPrimitive(fieldType.typeId) ||
         TypeIds.isContainer(fieldType.typeId) ||
         fieldType.typeId == TypeIds.string ||
@@ -1103,47 +1134,60 @@ final class TypeResolver {
         fieldType.typeId == TypeIds.namedUnion) {
       return TypeIds.union;
     }
+    final resolved = tryResolveFieldType(fieldType);
+    if (resolved != null && resolved.kind != RegistrationKind.builtin) {
+      switch (resolved.kind) {
+        case RegistrationKind.enumType:
+          return TypeIds.enumById;
+        case RegistrationKind.union:
+          return TypeIds.union;
+        case RegistrationKind.ext:
+        case RegistrationKind.struct:
+          return _typeIdFor(resolved);
+        case RegistrationKind.builtin:
+          break;
+      }
+    }
     return fieldType.ref ? TypeIds.unknown : fieldType.typeId;
   }
 
   @pragma('vm:prefer-inline')
-  WireTypeMeta _readTypeDef(
-    Buffer buffer, {
-    required List<TypeInfo> sharedTypes,
-    TypeInfo? expectedType,
-  }) {
+  TypeMeta _readTypeDef(Buffer buffer, {required List<TypeInfo> sharedTypes}) {
     final marker = buffer.readVarUint32Small14();
     final isRef = (marker & 1) == 1;
     final index = marker >>> 1;
     if (isRef) {
-      return wireTypeMetaForResolved(sharedTypes[index]);
+      return typeMetaForResolved(sharedTypes[index]);
     }
     final header = TypeHeader(buffer.readInt64());
-    final expectedTypeDef = expectedType?.typeDef;
-    if (expectedTypeDef != null && expectedTypeDef.header == header.value) {
-      // Header-cache hits intentionally skip without rehashing. Entries reach this cache only
-      // after a successful TypeDef parse and 52-bit metadata-hash validation.
-      header.skipRemaining(buffer);
-      sharedTypes.add(expectedType!);
-      return wireTypeMetaForResolved(expectedType);
-    }
     final cached = _parsedTypeMetaCache.lookup(header);
     if (cached != null) {
       // Header-cache hits intentionally skip without rehashing. Entries reach this cache only
-      // after a successful TypeDef parse and 52-bit metadata-hash validation.
+      // after a successful TypeDef parse and 52-bit metadata-hash validation. Do not add
+      // body/hash/schema-limit/exact-local checks here; the miss path owns them before publish.
       header.skipRemaining(buffer);
       sharedTypes.add(cached);
-      return wireTypeMetaForResolved(cached);
+      return typeMetaForResolved(cached);
     }
     final resolved = _readTypeDefWithHeader(buffer, header);
-    _parsedTypeMetaCache.remember(header, resolved);
     sharedTypes.add(resolved);
-    return wireTypeMetaForResolved(resolved);
+    return typeMetaForResolved(resolved);
   }
 
+  @pragma('vm:never-inline')
   TypeInfo _readTypeDefWithHeader(Buffer buffer, TypeHeader header) {
+    final typeDefStart = bufferReaderIndex(buffer) - 8;
     header.validateGlobal();
     final metaSize = header.readMetaSize(buffer);
+    if (metaSize > config.maxTypeMetaBytes) {
+      throw StateError(
+        'Type metadata body size $metaSize exceeds maxTypeMetaBytes '
+        '${config.maxTypeMetaBytes}. The data may be malicious. If the data '
+        'is not malicious, please increase maxTypeMetaBytes.',
+      );
+    }
+    // maxTypeMetaBytes is only a size cap. Prove the declared body bytes are
+    // readable before readBytes allocates/copies from the attacker-controlled size.
     buffer.checkReadableBytes(metaSize);
     final metaBody = buffer.readBytes(metaSize);
     final metaBytes = Buffer.wrap(metaBody);
@@ -1165,6 +1209,13 @@ final class TypeResolver {
       if (fieldCount == typeDefSmallFieldCountThreshold) {
         fieldCount += metaBytes.readVarUint32Small7();
       }
+      if (fieldCount > config.maxTypeFields) {
+        throw StateError(
+          'Type metadata field count $fieldCount exceeds maxTypeFields '
+          '${config.maxTypeFields}. The data may be malicious. If the data '
+          'is not malicious, please increase maxTypeFields.',
+        );
+      }
     } else {
       if ((classHeader & 0x70) != 0) {
         throw StateError('Invalid TypeDef kind header.');
@@ -1180,6 +1231,11 @@ final class TypeResolver {
     if (!byName) {
       userTypeId = metaBytes.readVarUint32();
     }
+    if (fieldCount > metaBytes.readableBytes) {
+      throw StateError(
+        'Type metadata field count exceeds available body bytes.',
+      );
+    }
     final fields = <FieldInfo>[];
     for (var i = 0; i < fieldCount; i += 1) {
       fields.add(_readTypeDefField(metaBytes));
@@ -1195,35 +1251,49 @@ final class TypeResolver {
         userTypeId != null
             ? resolveUserById(userTypeId)
             : resolveUserByEncodedName(encodedNamespace!, encodedTypeName!);
-    if (resolved.typeDef?.header != header.value &&
-        _typeDefTypeId(
-              resolved.kind,
-              byName: byName,
-              evolving: resolved.typeDef?.evolving ?? false,
-            ) !=
-            typeId) {
+    if (_typeDefTypeId(
+          resolved.kind,
+          byName: byName,
+          evolving: resolved.evolving,
+        ) !=
+        typeId) {
       throw StateError('TypeDef kind does not match registered type metadata.');
     }
+    final localTypeDef = typeDefForResolved(resolved);
+    if (_matchesEncodedTypeDef(buffer, typeDefStart, localTypeDef.encoded)) {
+      _parsedTypeMetaCache.remember(header, resolved);
+      return resolved;
+    }
     if (resolved.kind != RegistrationKind.struct) {
+      final remoteSchemaKey = _checkRemoteTypeDefLimit(
+        typeId: typeId,
+        userTypeId: userTypeId,
+        resolved: resolved,
+      );
+      _parsedTypeMetaCache.remember(header, resolved);
+      _recordRemoteTypeDef(remoteSchemaKey);
       return resolved;
     }
+    final remoteSchemaKey = _checkRemoteTypeDefLimit(
+      typeId: typeId,
+      userTypeId: userTypeId,
+      resolved: resolved,
+    );
     final remoteTypeDef = TypeDef(
       evolving: true,
       fields: List<FieldInfo>.unmodifiable(fields),
       header: header.value,
       encoded: Uint8List(0),
     );
-    final localTypeDef = resolved.typeDef;
-    if (localTypeDef != null && _sameTypeDef(localTypeDef, remoteTypeDef)) {
-      return resolved;
-    }
-    return TypeInfo(
+    final remoteResolved = TypeInfo(
       type: resolved.type,
       kind: resolved.kind,
       typeId: resolved.typeId,
       supportsRef: resolved.supportsRef,
       needsRootRef: resolved.needsRootRef,
       usesNestedTypeDefinitions: resolved.usesNestedTypeDefinitions,
+      evolving: resolved.evolving,
+      fields: resolved.fields,
       serializer: resolved.serializer,
       structSerializer: resolved.structSerializer,
       userTypeId: resolved.userTypeId,
@@ -1234,6 +1304,65 @@ final class TypeResolver {
       typeDef: resolved.typeDef,
       remoteTypeDef: remoteTypeDef,
     );
+    remoteResolved.structSerializer?.validateCompatibleTypeInfo(remoteResolved);
+    _parsedTypeMetaCache.remember(header, remoteResolved);
+    _recordRemoteTypeDef(remoteSchemaKey);
+    return remoteResolved;
+  }
+
+  bool _matchesEncodedTypeDef(
+    Buffer buffer,
+    int typeDefStart,
+    Uint8List encoded,
+  ) {
+    if (bufferReaderIndex(buffer) - typeDefStart != encoded.length) {
+      return false;
+    }
+    return bufferMatchesBytes(buffer, typeDefStart, encoded);
+  }
+
+  @pragma('vm:never-inline')
+  String _checkRemoteTypeDefLimit({
+    required int typeId,
+    required int? userTypeId,
+    required TypeInfo resolved,
+  }) {
+    final key =
+        userTypeId != null
+            ? 'i$userTypeId'
+            : 'n${resolved.namespace ?? ''}\u0000${resolved.typeName ?? ''}';
+    final versionsForType = _remoteSchemaVersionsByType[key] ?? 0;
+    if (versionsForType >= config.maxSchemaVersionsPerType) {
+      throw StateError(
+        'Remote schema version limit exceeded for one type. The data may be '
+        'malicious. If the data is not malicious, please increase '
+        'maxSchemaVersionsPerType=${config.maxSchemaVersionsPerType}.',
+      );
+    }
+    final acceptedTypeCount =
+        versionsForType == 0
+            ? _remoteSchemaVersionsByType.length + 1
+            : _remoteSchemaVersionsByType.length;
+    final averageLimit =
+        acceptedTypeCount * config.maxAverageSchemaVersionsPerType;
+    final globalLimit =
+        averageLimit > _minRemoteTypeMetaLimit
+            ? averageLimit
+            : _minRemoteTypeMetaLimit;
+    if (_totalAcceptedSchemaVersions >= globalLimit) {
+      throw StateError(
+        'Remote schema version limit exceeded globally. The data may be '
+        'malicious. If the data is not malicious, please increase '
+        'maxAverageSchemaVersionsPerType=${config.maxAverageSchemaVersionsPerType}.',
+      );
+    }
+    return key;
+  }
+
+  void _recordRemoteTypeDef(String key) {
+    final versionsForType = _remoteSchemaVersionsByType[key] ?? 0;
+    _remoteSchemaVersionsByType[key] = versionsForType + 1;
+    _totalAcceptedSchemaVersions += 1;
   }
 
   EncodedMetaString _readTypeDefName(
@@ -1320,8 +1449,8 @@ final class TypeResolver {
     );
   }
 
-  TypeInfo resolveBuiltinWireType(int wireTypeId) {
-    switch (wireTypeId) {
+  TypeInfo resolveBuiltinTypeId(int typeId) {
+    switch (typeId) {
       case TypeIds.boolType:
         return _builtin(bool, TypeIds.boolType);
       case TypeIds.int8:
@@ -1407,11 +1536,11 @@ final class TypeResolver {
       case TypeIds.float64Array:
         return _builtin(Float64List, TypeIds.float64Array);
       default:
-        throw StateError('Unsupported builtin wire type id $wireTypeId.');
+        throw StateError('Unsupported builtin type id $typeId.');
     }
   }
 
-  TypeInfo resolveExpectedRootWireType<T>(TypeInfo resolved) {
+  TypeInfo resolveExpectedRootType<T>(TypeInfo resolved) {
     if ((_isType<T, Int64>() || _isType<T, Int64?>()) &&
         resolved.typeId == TypeIds.varInt64) {
       return _builtin(Int64, TypeIds.varInt64);
@@ -1432,6 +1561,8 @@ final class TypeResolver {
       supportsRef: TypeIds.supportsRef(typeId),
       needsRootRef: false,
       usesNestedTypeDefinitions: false,
+      evolving: false,
+      fields: const <FieldInfo>[],
       serializer: _builtinSerializerFor(typeId, type),
       structSerializer: null,
       userTypeId: null,
@@ -1680,9 +1811,9 @@ final class TypeResolver {
     return internEncodedMetaString(encoded.bytes, encoding: encoded.encoding);
   }
 
-  void _rememberNamedType(int wireTypeId, TypeInfo resolved) {
-    if (wireTypeId < _lastNamedTypeByWireType.length) {
-      _lastNamedTypeByWireType[wireTypeId] = resolved;
+  void _rememberNamedType(int typeId, TypeInfo resolved) {
+    if (typeId < _lastNamedTypeById.length) {
+      _lastNamedTypeById[typeId] = resolved;
     }
     final namespace = resolved.encodedNamespace;
     final typeName = resolved.encodedTypeName;
@@ -1690,10 +1821,10 @@ final class TypeResolver {
       return;
     }
     final slot =
-        _namedTypeLookupCacheIndex(wireTypeId, namespace, typeName) &
+        _namedTypeLookupCacheIndex(typeId, namespace, typeName) &
         (_namedTypeLookupCache.length - 1);
     _namedTypeLookupCache[slot] = _NamedTypeReadCacheEntry(
-      wireTypeId,
+      typeId,
       namespace,
       typeName,
       resolved,
@@ -1701,44 +1832,11 @@ final class TypeResolver {
   }
 
   int _namedTypeLookupCacheIndex(
-    int wireTypeId,
+    int typeId,
     EncodedMetaString namespace,
     EncodedMetaString typeName,
   ) {
-    return Object.hash(wireTypeId, namespace.hash, typeName.hash);
-  }
-
-  bool _sameTypeDef(TypeDef left, TypeDef right) {
-    if (left.evolving != right.evolving ||
-        left.fields.length != right.fields.length) {
-      return false;
-    }
-    for (var index = 0; index < left.fields.length; index += 1) {
-      final leftField = left.fields[index];
-      final rightField = right.fields[index];
-      if (leftField.identifier != rightField.identifier ||
-          leftField.id != rightField.id ||
-          !_sameFieldType(leftField.fieldType, rightField.fieldType)) {
-        return false;
-      }
-    }
-    return true;
-  }
-
-  bool _sameFieldType(FieldType left, FieldType right) {
-    if (left.typeId != right.typeId ||
-        left.nullable != right.nullable ||
-        left.ref != right.ref ||
-        left.dynamic != right.dynamic ||
-        left.arguments.length != right.arguments.length) {
-      return false;
-    }
-    for (var index = 0; index < left.arguments.length; index += 1) {
-      if (!_sameFieldType(left.arguments[index], right.arguments[index])) {
-        return false;
-      }
-    }
-    return true;
+    return Object.hash(typeId, namespace.hash, typeName.hash);
   }
 
   Type _builtinTypeForFieldType(FieldType fieldType) {
@@ -1778,19 +1876,19 @@ final class TypeResolver {
         declaredTypeName.endsWith('.$typeName');
   }
 
-  bool _matchesNamedWireType(TypeInfo resolved, int wireTypeId) {
+  bool _matchesNamedTypeId(TypeInfo resolved, int typeId) {
     if (!resolved.isNamed) {
       return false;
     }
     switch (resolved.kind) {
       case RegistrationKind.enumType:
-        return wireTypeId == TypeIds.namedEnum;
+        return typeId == TypeIds.namedEnum;
       case RegistrationKind.struct:
-        return wireTypeId == TypeIds.namedStruct;
+        return typeId == TypeIds.namedStruct;
       case RegistrationKind.ext:
-        return wireTypeId == TypeIds.namedExt;
+        return typeId == TypeIds.namedExt;
       case RegistrationKind.union:
-        return wireTypeId == TypeIds.namedUnion;
+        return typeId == TypeIds.namedUnion;
       case RegistrationKind.builtin:
         return false;
     }
@@ -1815,13 +1913,13 @@ final class _BuiltinKey {
 }
 
 final class _NamedTypeReadCacheEntry {
-  final int wireTypeId;
+  final int typeId;
   final EncodedMetaString namespace;
   final EncodedMetaString typeName;
   final TypeInfo resolved;
 
   const _NamedTypeReadCacheEntry(
-    this.wireTypeId,
+    this.typeId,
     this.namespace,
     this.typeName,
     this.resolved,
diff --git a/dart/packages/fory/lib/src/serializer/collection_serializers.dart b/dart/packages/fory/lib/src/serializer/collection_serializers.dart
index 66ab8da9e..4e2a8050c 100644
--- a/dart/packages/fory/lib/src/serializer/collection_serializers.dart
+++ b/dart/packages/fory/lib/src/serializer/collection_serializers.dart
@@ -301,7 +301,7 @@ final class ListSerializer extends Serializer<List> {
     if (!usesDeclaredType &&
         sameTypeInfo != null &&
         analysis.firstNonNull != null) {
-      context.writeTypeMetaValue(sameTypeInfo, analysis.firstNonNull!);
+      context.writeTypeMetaValue(sameTypeInfo);
     }
     if (declaredTypeInfo != null) {
       _writeSameTypeElements(
@@ -747,7 +747,7 @@ void writeTypedListPayload<T>(
     ),
   );
   if (!usesDeclaredType) {
-    context.writeTypeMetaValue(declaredTypeInfo, values.first as Object);
+    context.writeTypeMetaValue(declaredTypeInfo);
   }
   _writeSameTypeElements(
     context,
diff --git a/dart/packages/fory/lib/src/serializer/map_serializers.dart b/dart/packages/fory/lib/src/serializer/map_serializers.dart
index 18d511412..051454c3d 100644
--- a/dart/packages/fory/lib/src/serializer/map_serializers.dart
+++ b/dart/packages/fory/lib/src/serializer/map_serializers.dart
@@ -154,10 +154,10 @@ final class MapSerializer extends Serializer<Map> {
       context.buffer.writeUint8(0);
       final chunkLengthOffset = bufferWriterIndex(context.buffer) - 1;
       if (!keyDeclared) {
-        context.writeTypeMetaValue(chunkKeyTypeInfo, key);
+        context.writeTypeMetaValue(chunkKeyTypeInfo);
       }
       if (!valueDeclared) {
-        context.writeTypeMetaValue(chunkValueTypeInfo, value);
+        context.writeTypeMetaValue(chunkValueTypeInfo);
       }
       var chunkLength = 1;
       final tracksDepth =
diff --git a/dart/packages/fory/lib/src/serializer/serialization_field_info.dart b/dart/packages/fory/lib/src/serializer/serialization_field_info.dart
index fc175df49..75662774f 100644
--- a/dart/packages/fory/lib/src/serializer/serialization_field_info.dart
+++ b/dart/packages/fory/lib/src/serializer/serialization_field_info.dart
@@ -35,8 +35,8 @@ final class SerializationFieldInfo {
     required this.index,
     TypeInfo? declaredTypeInfo,
     bool? usesDeclaredType,
-  })  : _declaredTypeInfo = declaredTypeInfo,
-        _usesDeclaredType = usesDeclaredType;
+  }) : _declaredTypeInfo = declaredTypeInfo,
+       _usesDeclaredType = usesDeclaredType;
 
   String get name => field.name;
 
diff --git a/dart/packages/fory/lib/src/serializer/struct_serializer.dart b/dart/packages/fory/lib/src/serializer/struct_serializer.dart
index beb0d2c1f..0a50cc646 100644
--- a/dart/packages/fory/lib/src/serializer/struct_serializer.dart
+++ b/dart/packages/fory/lib/src/serializer/struct_serializer.dart
@@ -72,14 +72,6 @@ final class StructSerializer extends Serializer<Object?> {
 
   List<SerializationFieldInfo> get localFields => _localFields;
 
-  TypeDef typeDefForWrite(
-    WriteContext context,
-    TypeInfo resolved,
-    Object value,
-  ) {
-    return resolved.typeDef!;
-  }
-
   void writeValue(WriteContext context, TypeInfo resolved, Object value) {
     if (!context.config.compatible && context.config.checkStructVersion) {
       context.buffer.writeUint32(schemaHash(_typeDef));
@@ -93,9 +85,9 @@ final class StructSerializer extends Serializer<Object?> {
     TypeInfo resolved, {
     bool hasCurrentPreservedRef = false,
   }) {
-    if (context.config.compatible &&
-        resolved.isCompatibleStruct &&
-        resolved.remoteTypeDef != null) {
+    if (resolved.remoteTypeDef != null &&
+        context.config.compatible &&
+        resolved.isCompatibleStruct) {
       return _readCompatible(context, resolved);
     }
     return readSameTypeValue(context, resolved);
@@ -138,6 +130,13 @@ final class StructSerializer extends Serializer<Object?> {
     return value;
   }
 
+  @pragma('vm:never-inline')
+  void validateCompatibleTypeInfo(TypeInfo resolved) {
+    if (resolved.remoteTypeDef != null) {
+      _compatibleReadLayoutForResolved(resolved);
+    }
+  }
+
   @pragma('vm:prefer-inline')
   CompatibleStructReadLayout _compatibleReadLayoutForResolved(
     TypeInfo resolved,
@@ -316,8 +315,8 @@ bool _compatibleFieldType(
         ) !=
         null;
   }
-  if (_isStructWireType(localType.typeId) &&
-      _isStructWireType(remoteType.typeId) &&
+  if (_isStructTypeId(localType.typeId) &&
+      _isStructTypeId(remoteType.typeId) &&
       localType.arguments.length == remoteType.arguments.length) {
     return true;
   }
@@ -328,8 +327,7 @@ bool _compatibleFieldType(
       localType.typeId == remoteType.typeId ||
       _compatibleUnknownUserType(localType, remoteType) ||
       _compatibleGeneratedManualUserType(localType, remoteType) ||
-      (_isStructWireType(localType.typeId) &&
-          _isStructWireType(remoteType.typeId));
+      (_isStructTypeId(localType.typeId) && _isStructTypeId(remoteType.typeId));
   if (!sameWireFamily ||
       localType.arguments.length != remoteType.arguments.length) {
     return false;
@@ -373,13 +371,13 @@ bool _hasUnsupportedListArrayMismatch(
   return false;
 }
 
-bool _isStructWireType(int typeId) =>
+bool _isStructTypeId(int typeId) =>
     typeId == TypeIds.struct ||
     typeId == TypeIds.compatibleStruct ||
     typeId == TypeIds.namedStruct ||
     typeId == TypeIds.namedCompatibleStruct;
 
-bool _isManualUserWireType(int typeId) =>
+bool _isManualUserTypeId(int typeId) =>
     typeId == TypeIds.ext ||
     typeId == TypeIds.namedExt ||
     typeId == TypeIds.union ||
@@ -393,10 +391,10 @@ bool _compatibleGeneratedManualUserType(
   if (localType.arguments.isNotEmpty || remoteType.arguments.isNotEmpty) {
     return false;
   }
-  return (_isStructWireType(localType.typeId) &&
-          _isManualUserWireType(remoteType.typeId)) ||
-      (_isManualUserWireType(localType.typeId) &&
-          _isStructWireType(remoteType.typeId));
+  return (_isStructTypeId(localType.typeId) &&
+          _isManualUserTypeId(remoteType.typeId)) ||
+      (_isManualUserTypeId(localType.typeId) &&
+          _isStructTypeId(remoteType.typeId));
 }
 
 bool _compatibleUnknownUserType(FieldType localType, FieldType remoteType) {
diff --git a/dart/packages/fory/test/enum_union_serializer_test.dart b/dart/packages/fory/test/enum_union_serializer_test.dart
index 28234a7e3..ab5da7b26 100644
--- a/dart/packages/fory/test/enum_union_serializer_test.dart
+++ b/dart/packages/fory/test/enum_union_serializer_test.dart
@@ -199,6 +199,28 @@ void main() {
       expect(envelopeRoundTrip.color, equals(SimpleColor.green));
     });
 
+    test('id registered enums do not use TypeDef metadata limits', () {
+      final fory = Fory(maxTypeMetaBytes: 1, maxSchemaVersionsPerType: 1);
+      EnumUnionSerializerTestForyModule.register(fory, SimpleColor, id: 101);
+
+      final decoded = fory.deserialize<SimpleColor>(
+        fory.serialize(SimpleColor.green),
+      );
+
+      expect(decoded, equals(SimpleColor.green));
+    });
+
+    test('id registered unions do not use TypeDef metadata limits', () {
+      final fory = Fory(maxTypeMetaBytes: 1, maxSchemaVersionsPerType: 1);
+      fory.registerSerializer(TestUnion, const TestUnionSerializer(), id: 102);
+
+      final decoded = fory.deserialize<TestUnion>(
+        fory.serialize(TestUnion.ofString('alpha')),
+      );
+
+      expect(decoded, equals(TestUnion.ofString('alpha')));
+    });
+
     test('uses raw enum values across reorder and rename', () {
       final writer = Fory();
       final reader = Fory();
diff --git a/dart/packages/fory/test/scalar_and_typed_array_serializer_test.dart b/dart/packages/fory/test/scalar_and_typed_array_serializer_test.dart
index 8469cdd87..7ef820c66 100644
--- a/dart/packages/fory/test/scalar_and_typed_array_serializer_test.dart
+++ b/dart/packages/fory/test/scalar_and_typed_array_serializer_test.dart
@@ -530,7 +530,7 @@ void main() {
 
       for (final value in cases) {
         final roundTrip = fory.deserialize<double>(
-          fory.serializeBuiltin(value, wireTypeId: TypeIds.float16),
+          fory.serializeBuiltin(value, typeId: TypeIds.float16),
         );
         expect(toFloat16Bits(roundTrip), equals(toFloat16Bits(value)));
       }
@@ -562,7 +562,7 @@ void main() {
 
       for (final value in cases) {
         final roundTrip = fory.deserialize<double>(
-          fory.serializeBuiltin(value, wireTypeId: TypeIds.bfloat16),
+          fory.serializeBuiltin(value, typeId: TypeIds.bfloat16),
         );
         expect(toBfloat16Bits(roundTrip), equals(toBfloat16Bits(value)));
       }
diff --git a/dart/packages/fory/test/xlang_protocol_test.dart b/dart/packages/fory/test/xlang_protocol_test.dart
index 99a2d63a0..21e2ada7d 100644
--- a/dart/packages/fory/test/xlang_protocol_test.dart
+++ b/dart/packages/fory/test/xlang_protocol_test.dart
@@ -17,9 +17,15 @@
  * under the License.
  */
 
+import 'dart:collection';
 import 'dart:typed_data';
 
 import 'package:fory/fory.dart';
+import 'package:fory/src/codegen/generated_registry.dart';
+import 'package:fory/src/context/meta_string_reader.dart';
+import 'package:fory/src/context/meta_string_writer.dart';
+import 'package:fory/src/meta/field_info.dart';
+import 'package:fory/src/meta/type_def.dart';
 import 'package:fory/src/meta/type_meta.dart';
 import 'package:fory/src/resolver/type_resolver.dart';
 import 'package:fory/src/util/hash_util.dart';
@@ -38,6 +44,219 @@ final class _CacheTestSerializer extends Serializer<Object?> {
   void write(WriteContext context, Object? value) {}
 }
 
+final class _SchemaLocal {}
+
+final class _SchemaRemoteA {}
+
+final class _SchemaRemoteB {}
+
+final class _SchemaRemoteC {}
+
+final class _LateDartExt {}
+
+final class _LateDartHolder {}
+
+const _intFieldType = GeneratedFieldType(
+  type: int,
+  typeId: TypeIds.int32,
+  nullable: false,
+  ref: false,
+  dynamic: false,
+  arguments: <GeneratedFieldType>[],
+);
+
+const _mapFieldType = GeneratedFieldType(
+  type: Map<String, int>,
+  typeId: TypeIds.map,
+  nullable: false,
+  ref: false,
+  dynamic: false,
+  arguments: <GeneratedFieldType>[
+    GeneratedFieldType(
+      type: String,
+      typeId: TypeIds.string,
+      nullable: false,
+      ref: false,
+      dynamic: false,
+      arguments: <GeneratedFieldType>[],
+    ),
+    GeneratedFieldType(
+      type: int,
+      typeId: TypeIds.int32,
+      nullable: false,
+      ref: false,
+      dynamic: false,
+      arguments: <GeneratedFieldType>[],
+    ),
+  ],
+);
+
+const _lateExtFieldType = GeneratedFieldType(
+  type: _LateDartExt,
+  declaredTypeName: '_LateDartExt',
+  typeId: TypeIds.compatibleStruct,
+  nullable: false,
+  ref: true,
+  dynamic: false,
+  arguments: <GeneratedFieldType>[],
+);
+
+GeneratedFieldInfo _generatedField(String name) => GeneratedFieldInfo(
+  name: name,
+  identifier: name,
+  id: null,
+  fieldType: _intFieldType,
+);
+
+GeneratedFieldInfo _generatedMapField(String name) => GeneratedFieldInfo(
+  name: name,
+  identifier: name,
+  id: null,
+  fieldType: _mapFieldType,
+);
+
+void _rememberSchema(Type type, List<GeneratedFieldInfo> fields) {
+  GeneratedTypeCatalog.remember(
+    type,
+    GeneratedTypeEntry(
+      kind: GeneratedTypeKind.struct,
+      serializerFactory: () => const _CacheTestSerializer(),
+      evolving: true,
+      needsRootRef: false,
+      usesNestedTypeDefinitions: false,
+      fields: fields.map((field) => field.toFieldInfo()).toList(),
+    ),
+  );
+}
+
+void _rememberEnum(Type type) {
+  GeneratedTypeCatalog.remember(
+    type,
+    GeneratedTypeEntry(
+      kind: GeneratedTypeKind.enumType,
+      serializerFactory: () => const _CacheTestSerializer(),
+    ),
+  );
+}
+
+void _rememberLateHolder() {
+  _rememberSchema(_LateDartHolder, <GeneratedFieldInfo>[
+    const GeneratedFieldInfo(
+      name: 'value',
+      identifier: 'value',
+      id: null,
+      fieldType: _lateExtFieldType,
+    ),
+  ]);
+}
+
+Uint8List _lateHolderTypeDefBytes({required bool registerExtFirst}) {
+  final resolver = TypeResolver(const Config());
+  _rememberLateHolder();
+  if (registerExtFirst) {
+    resolver.registerSerializer(
+      _LateDartExt,
+      const _CacheTestSerializer(),
+      namespace: 'example',
+      typeName: 'LateDartExt',
+    );
+  }
+  resolver.registerGenerated(
+    _LateDartHolder,
+    namespace: 'example',
+    typeName: 'LateDartHolder',
+  );
+  if (!registerExtFirst) {
+    resolver.registerSerializer(
+      _LateDartExt,
+      const _CacheTestSerializer(),
+      namespace: 'example',
+      typeName: 'LateDartExt',
+    );
+  }
+  final resolved = resolver.resolveUserByName('example', 'LateDartHolder');
+  return resolver.typeDefForResolved(resolved).encoded;
+}
+
+Uint8List _typeMetaBytes(
+  Type type,
+  String name,
+  List<GeneratedFieldInfo> fields,
+) {
+  final resolver = TypeResolver(const Config());
+  _rememberSchema(type, fields);
+  final parts = name.split('.');
+  resolver.registerGenerated(
+    type,
+    namespace: parts.first,
+    typeName: parts.last,
+  );
+  final resolved = resolver.resolveUserByName(parts.first, parts.last);
+  final buffer = Buffer();
+  resolver.writeTypeMeta(
+    buffer,
+    resolved,
+    typeDefIds: LinkedHashMap<TypeDef, int>.identity(),
+    metaStringWriter: MetaStringWriter(),
+  );
+  return buffer.toBytes();
+}
+
+Uint8List _enumTypeMetaBytes(Type type, String name) {
+  final resolver = TypeResolver(const Config());
+  _rememberEnum(type);
+  final parts = name.split('.');
+  resolver.registerGenerated(
+    type,
+    namespace: parts.first,
+    typeName: parts.last,
+  );
+  final resolved = resolver.resolveUserByName(parts.first, parts.last);
+  final buffer = Buffer();
+  resolver.writeTypeMeta(
+    buffer,
+    resolved,
+    typeDefIds: LinkedHashMap<TypeDef, int>.identity(),
+    metaStringWriter: MetaStringWriter(),
+  );
+  return buffer.toBytes();
+}
+
+TypeInfo _cachedTypeInfo(Int64 header) {
+  return TypeInfo(
+    type: Object,
+    kind: RegistrationKind.builtin,
+    typeId: TypeIds.struct,
+    supportsRef: false,
+    needsRootRef: false,
+    usesNestedTypeDefinitions: false,
+    evolving: false,
+    fields: const <FieldInfo>[],
+    serializer: const _CacheTestSerializer(),
+    structSerializer: null,
+    userTypeId: null,
+    namespace: null,
+    typeName: null,
+    encodedNamespace: null,
+    encodedTypeName: null,
+    typeDef: TypeDef(
+      evolving: false,
+      fields: const <FieldInfo>[],
+      header: header,
+      encoded: Uint8List(0),
+    ),
+    remoteTypeDef: null,
+  );
+}
+
+void _readTypeMeta(TypeResolver resolver, Uint8List bytes) {
+  resolver.readTypeMeta(
+    Buffer.wrap(bytes),
+    sharedTypes: <TypeInfo>[],
+    metaStringReader: MetaStringReader(resolver),
+  );
+}
+
 void main() {
   group('xlang protocol regressions', () {
     test('deserializes NONE wire values as null', () {
@@ -51,17 +270,18 @@ void main() {
     test('deserializes FLOAT16_ARRAY wire values', () {
       final fory = Fory();
       final bytes = Uint8List.fromList(
-        fory.serialize(
-          Uint16List.fromList(<int>[0x3c00, 0xc000, 0x7e00]),
-        ),
+        fory.serialize(Uint16List.fromList(<int>[0x3c00, 0xc000, 0x7e00])),
       );
       bytes[2] = TypeIds.float16Array;
 
       final values = fory.deserialize<Float16List>(bytes);
 
       expect(
-        Uint16List.view(values.buffer, values.offsetInBytes, values.length)
-            .toList(),
+        Uint16List.view(
+          values.buffer,
+          values.offsetInBytes,
+          values.length,
+        ).toList(),
         orderedEquals(<int>[0x3c00, 0xc000, 0x7e00]),
       );
     });
@@ -71,7 +291,7 @@ void main() {
       final scalarBytes = Uint8List.fromList(
         fory.serializeBuiltin(
           fromBfloat16Bits(0xbf60),
-          wireTypeId: TypeIds.bfloat16,
+          typeId: TypeIds.bfloat16,
         ),
       );
       final rawArray = Uint16List.fromList(<int>[0x3f80, 0xbf80, 0x7fc1]);
@@ -94,9 +314,9 @@ void main() {
       );
     });
 
-    test('serializes root builtins with an explicit wire type', () {
+    test('serializes root builtins with an explicit xlang type', () {
       final fory = Fory();
-      final bytes = fory.serializeBuiltin(7, wireTypeId: TypeIds.varInt32);
+      final bytes = fory.serializeBuiltin(7, typeId: TypeIds.varInt32);
 
       expect(bytes[0], equals(0x01));
       expect(bytes[1], equals(0xff));
@@ -121,37 +341,167 @@ void main() {
       );
     });
 
-    test('parsed TypeDef cache stops publishing at capacity', () {
-      const resolved = TypeInfo(
-        type: Object,
-        kind: RegistrationKind.builtin,
-        typeId: TypeIds.struct,
-        supportsRef: false,
-        needsRootRef: false,
-        usesNestedTypeDefinitions: false,
-        serializer: _CacheTestSerializer(),
-        structSerializer: null,
-        userTypeId: null,
-        namespace: null,
-        typeName: null,
-        encodedNamespace: null,
-        encodedTypeName: null,
-        typeDef: null,
-        remoteTypeDef: null,
-      );
+    test('parsed TypeDef cache publishes beyond old implementation floor', () {
       final cache = ParsedTypeMetaCache();
-      for (var i = 0; i < ParsedTypeMetaCache.maxEntries; i++) {
-        cache.remember(TypeHeader(Int64(i)), resolved);
+      const oldImplementationFloor = 8192;
+      late TypeInfo lastResolved;
+      for (var i = 0; i < oldImplementationFloor; i++) {
+        final header = TypeHeader(Int64(i));
+        final resolved = _cachedTypeInfo(header.value);
+        cache.remember(header, resolved);
+        lastResolved = resolved;
       }
 
       expect(
-        cache.lookup(TypeHeader(Int64(ParsedTypeMetaCache.maxEntries - 1))),
-        same(resolved),
+        cache.lookup(TypeHeader(Int64(oldImplementationFloor - 1))),
+        same(lastResolved),
+      );
+      final aboveOldFloor = TypeHeader(Int64(oldImplementationFloor));
+      final aboveOldFloorResolved = _cachedTypeInfo(aboveOldFloor.value);
+      cache.remember(aboveOldFloor, aboveOldFloorResolved);
+
+      expect(cache.lookup(aboveOldFloor), same(aboveOldFloorResolved));
+    });
+
+    test('TypeDef uses late registered field type', () {
+      expect(
+        _lateHolderTypeDefBytes(registerExtFirst: false),
+        orderedEquals(_lateHolderTypeDefBytes(registerExtFirst: true)),
+      );
+    });
+
+    test('remote schema limit rejects extra versions', () {
+      const name = 'example.Unknown';
+      final reader = TypeResolver(const Config(maxSchemaVersionsPerType: 1));
+      _rememberSchema(_SchemaLocal, <GeneratedFieldInfo>[]);
+      reader.registerGenerated(
+        _SchemaLocal,
+        namespace: 'example',
+        typeName: 'Unknown',
+      );
+      final first = _typeMetaBytes(_SchemaRemoteA, name, <GeneratedFieldInfo>[
+        _generatedField('firstValue'),
+      ]);
+      final second = _typeMetaBytes(_SchemaRemoteB, name, <GeneratedFieldInfo>[
+        _generatedField('secondValue'),
+      ]);
+
+      _readTypeMeta(reader, first);
+
+      expect(() => _readTypeMeta(reader, second), throwsA(isA<StateError>()));
+    });
+
+    test('named enum TypeDef uses metadata byte limit', () {
+      const name = 'example.RemoteEnum';
+      final reader = TypeResolver(const Config(maxTypeMetaBytes: 1));
+      _rememberEnum(_SchemaLocal);
+      final bytes = _enumTypeMetaBytes(_SchemaRemoteA, name);
+
+      expect(() => _readTypeMeta(reader, bytes), throwsA(isA<StateError>()));
+    });
+
+    test('registered named enum TypeDef uses metadata byte limit', () {
+      const name = 'example.RemoteEnum';
+      final reader = TypeResolver(const Config(maxTypeMetaBytes: 1));
+      _rememberEnum(_SchemaLocal);
+      reader.registerGenerated(
+        _SchemaLocal,
+        namespace: 'example',
+        typeName: 'RemoteEnum',
+      );
+      final bytes = _enumTypeMetaBytes(_SchemaRemoteA, name);
+
+      expect(() => _readTypeMeta(reader, bytes), throwsA(isA<StateError>()));
+    });
+
+    test('exact local named enum TypeDef is accepted', () {
+      const name = 'example.SharedEnum';
+      final reader = TypeResolver(const Config(maxSchemaVersionsPerType: 1));
+      _rememberEnum(_SchemaLocal);
+      reader.registerGenerated(
+        _SchemaLocal,
+        namespace: 'example',
+        typeName: 'SharedEnum',
+      );
+      final bytes = _enumTypeMetaBytes(_SchemaLocal, name);
+
+      _readTypeMeta(reader, bytes);
+    });
+
+    test('type meta field limit rejects large struct', () {
+      final reader = TypeResolver(const Config(maxTypeFields: 1));
+      final bytes = _typeMetaBytes(
+        _SchemaRemoteA,
+        'example.TooManyFields',
+        <GeneratedFieldInfo>[
+          _generatedField('firstValue'),
+          _generatedField('secondValue'),
+        ],
+      );
+
+      expect(() => _readTypeMeta(reader, bytes), throwsA(isA<StateError>()));
+    });
+
+    test('type meta body limit rejects large metadata', () {
+      final reader = TypeResolver(const Config(maxTypeMetaBytes: 1));
+      final bytes = _typeMetaBytes(
+        _SchemaRemoteA,
+        'example.LargeTypeMeta',
+        <GeneratedFieldInfo>[_generatedField('value')],
+      );
+
+      expect(() => _readTypeMeta(reader, bytes), throwsA(isA<StateError>()));
+    });
+
+    test('remote schema limit keeps unknown types separate', () {
+      final reader = TypeResolver(const Config(maxSchemaVersionsPerType: 1));
+      _rememberSchema(_SchemaLocal, <GeneratedFieldInfo>[]);
+      reader.registerGenerated(
+        _SchemaLocal,
+        namespace: 'example',
+        typeName: 'UnknownA',
+      );
+      _rememberSchema(_SchemaRemoteC, <GeneratedFieldInfo>[]);
+      reader.registerGenerated(
+        _SchemaRemoteC,
+        namespace: 'example',
+        typeName: 'UnknownB',
+      );
+      final first = _typeMetaBytes(
+        _SchemaRemoteA,
+        'example.UnknownA',
+        <GeneratedFieldInfo>[_generatedField('firstValue')],
+      );
+      final second = _typeMetaBytes(
+        _SchemaRemoteB,
+        'example.UnknownB',
+        <GeneratedFieldInfo>[_generatedField('secondValue')],
+      );
+
+      _readTypeMeta(reader, first);
+      _readTypeMeta(reader, second);
+    });
+
+    test('failed remote schema does not consume schema limit', () {
+      const name = 'example.Accepted';
+      final reader = TypeResolver(const Config(maxSchemaVersionsPerType: 1));
+      _rememberSchema(_SchemaLocal, <GeneratedFieldInfo>[
+        _generatedField('value'),
+      ]);
+      reader.registerGenerated(
+        _SchemaLocal,
+        namespace: 'example',
+        typeName: 'Accepted',
       );
-      final uncached = TypeHeader(Int64(ParsedTypeMetaCache.maxEntries));
-      cache.remember(uncached, resolved);
+      final invalid = _typeMetaBytes(_SchemaRemoteA, name, <GeneratedFieldInfo>[
+        _generatedMapField('value'),
+      ]);
+      final valid = _typeMetaBytes(_SchemaRemoteB, name, <GeneratedFieldInfo>[
+        _generatedField('extraValue'),
+      ]);
 
-      expect(cache.lookup(uncached), isNull);
+      expect(() => _readTypeMeta(reader, invalid), throwsA(isA<StateError>()));
+      _readTypeMeta(reader, valid);
     });
 
     test('validates parsed TypeDef body hash before caching', () {
diff --git a/docs/guide/cpp/configuration.md b/docs/guide/cpp/configuration.md
index 478e82f8f..d61745004 100644
--- a/docs/guide/cpp/configuration.md
+++ b/docs/guide/cpp/configuration.md
@@ -115,6 +115,56 @@ This limits the maximum depth for nested polymorphic object serialization (e.g.,
 - **Increase**: For legitimate deeply nested data structures
 - **Decrease**: For stricter security requirements or shallow data structures
 
+### max_schema_versions_per_type(uint32_t)
+
+Set the maximum accepted remote metadata versions for one logical type.
+
+```cpp
+auto fory = Fory::builder()
+    .max_schema_versions_per_type(10)
+    .build();
+```
+
+**Default:** `10`
+
+### max_type_fields(uint32_t)
+
+Set the maximum fields accepted in one received remote struct metadata body.
+
+```cpp
+auto fory = Fory::builder()
+    .max_type_fields(512)
+    .build();
+```
+
+**Default:** `512`
+
+### max_type_meta_bytes(uint32_t)
+
+Set the maximum encoded body bytes accepted for one received TypeDef body,
+excluding the 8-byte header and any extended-size varint.
+
+```cpp
+auto fory = Fory::builder()
+    .max_type_meta_bytes(4096)
+    .build();
+```
+
+**Default:** `4096`
+
+### max_average_schema_versions_per_type(uint32_t)
+
+Set the average accepted remote metadata versions across accepted remote types.
+The effective global floor is `8192` schemas.
+
+```cpp
+auto fory = Fory::builder()
+    .max_average_schema_versions_per_type(3)
+    .build();
+```
+
+**Default:** `3`
+
 ### check_struct_version(bool)
 
 Enable/disable struct version checking.
@@ -150,13 +200,17 @@ auto fory = Fory::builder().build_thread_safe();  // Returns ThreadSafeFory
 
 ## Configuration Summary
 
-| Option                       | Description                             | Default |
-| ---------------------------- | --------------------------------------- | ------- |
-| `xlang(bool)`                | Use xlang mode                          | `true`  |
-| `compatible(bool)`           | Enable schema evolution                 | `true`  |
-| `track_ref(bool)`            | Enable reference tracking               | `true`  |
-| `max_dyn_depth(uint32_t)`    | Maximum nesting depth for dynamic types | `5`     |
-| `check_struct_version(bool)` | Enable struct version checking          | `false` |
+| Option                                           | Description                                       | Default |
+| ------------------------------------------------ | ------------------------------------------------- | ------- |
+| `xlang(bool)`                                    | Use xlang mode                                    | `true`  |
+| `compatible(bool)`                               | Enable schema evolution                           | `true`  |
+| `track_ref(bool)`                                | Enable reference tracking                         | `true`  |
+| `max_dyn_depth(uint32_t)`                        | Maximum nesting depth for dynamic types           | `5`     |
+| `max_type_fields(uint32_t)`                      | Max fields in one received struct metadata body   | `512`   |
+| `max_type_meta_bytes(uint32_t)`                  | Max encoded bytes in one received metadata body   | `4096`  |
+| `max_schema_versions_per_type(uint32_t)`         | Max remote metadata versions for one logical type | `10`    |
+| `max_average_schema_versions_per_type(uint32_t)` | Average remote metadata versions across types     | `3`     |
+| `check_struct_version(bool)`                     | Enable struct version checking                    | `false` |
 
 ## Security
 
@@ -166,6 +220,8 @@ Security-related configuration:
 - Use `check_struct_version(true)` with `compatible(false)` for intentional same-schema payloads.
 - Keep `max_dyn_depth(...)` as low as your model permits to reject unexpectedly deep polymorphic
   graphs.
+- Keep the remote schema metadata limits at their defaults unless the data is not malicious and a
+  trusted peer sends larger metadata or many schema versions.
 - Prefer concrete fields over broad polymorphic fields for untrusted input.
 
 ## Related Topics
diff --git a/docs/guide/csharp/configuration.md b/docs/guide/csharp/configuration.md
index a158a6d5e..e7c0c24d4 100644
--- a/docs/guide/csharp/configuration.md
+++ b/docs/guide/csharp/configuration.md
@@ -35,12 +35,16 @@ ThreadSafeFory threadSafe = Fory.Builder().BuildThreadSafe();
 
 `Fory.Builder().Build()` uses:
 
-| Option               | Default | Description                                  |
-| -------------------- | ------- | -------------------------------------------- |
-| `TrackRef`           | `false` | Reference tracking disabled                  |
-| `Compatible`         | `true`  | Compatible schema-evolution metadata enabled |
-| `CheckStructVersion` | `false` | Struct schema hash checks disabled           |
-| `MaxDepth`           | `20`    | Max dynamic nesting depth                    |
+| Option                            | Default | Description                                       |
+| --------------------------------- | ------- | ------------------------------------------------- |
+| `TrackRef`                        | `false` | Reference tracking disabled                       |
+| `Compatible`                      | `true`  | Compatible schema-evolution metadata enabled      |
+| `CheckStructVersion`              | `false` | Struct schema hash checks disabled                |
+| `MaxDepth`                        | `20`    | Max dynamic nesting depth                         |
+| `MaxTypeFields`                   | `512`   | Max fields in one received struct metadata body   |
+| `MaxTypeMetaBytes`                | `4096`  | Max encoded bytes in one received metadata body   |
+| `MaxSchemaVersionsPerType`        | `10`    | Max remote metadata versions for one logical type |
+| `MaxAverageSchemaVersionsPerType` | `3`     | Average remote metadata versions across types     |
 
 ## Builder Options
 
@@ -92,6 +96,48 @@ Fory fory = Fory.Builder()
 
 `value` must be greater than `0`.
 
+### `MaxTypeFields(int value)`
+
+Sets the maximum fields accepted in one received remote struct metadata body.
+
+```csharp
+Fory fory = Fory.Builder()
+    .MaxTypeFields(512)
+    .Build();
+```
+
+### `MaxTypeMetaBytes(int value)`
+
+Sets the maximum encoded body bytes accepted for one received TypeMeta body,
+excluding the 8-byte header and any extended-size varint.
+
+```csharp
+Fory fory = Fory.Builder()
+    .MaxTypeMetaBytes(4096)
+    .Build();
+```
+
+### `MaxSchemaVersionsPerType(int value)`
+
+Sets the maximum accepted remote metadata versions for one logical type.
+
+```csharp
+Fory fory = Fory.Builder()
+    .MaxSchemaVersionsPerType(10)
+    .Build();
+```
+
+### `MaxAverageSchemaVersionsPerType(int value)`
+
+Sets the average accepted remote metadata versions across accepted remote types.
+The effective global floor is `8192` schemas.
+
+```csharp
+Fory fory = Fory.Builder()
+    .MaxAverageSchemaVersionsPerType(3)
+    .Build();
+```
+
 ## Common Configurations
 
 ### Compatible service
@@ -127,6 +173,8 @@ Security-related configuration:
 - Register only the expected types before deserializing untrusted payloads.
 - Use `CheckStructVersion(true)` with `Compatible(false)` for intentional same-schema payloads.
 - Set `MaxDepth(...)` to reject unexpectedly deep dynamic object graphs.
+- Keep the remote schema metadata limits at their defaults unless the data is not malicious and a
+  trusted peer sends larger metadata or many schema versions.
 - Prefer generated or registered concrete models over broad dynamic fields for untrusted input.
 
 ## Related Topics
diff --git a/docs/guide/dart/configuration.md b/docs/guide/dart/configuration.md
index f84ff49c2..6a4c640f6 100644
--- a/docs/guide/dart/configuration.md
+++ b/docs/guide/dart/configuration.md
@@ -34,6 +34,10 @@ final fory = Fory();
 // customize limits while keeping default compatible mode
 final fory = Fory(
   maxDepth: 512,
+  maxTypeFields: 512,
+  maxTypeMetaBytes: 4096,
+  maxSchemaVersionsPerType: 10,
+  maxAverageSchemaVersionsPerType: 3,
 );
 ```
 
@@ -82,13 +86,38 @@ Limits how deeply nested an object graph can be. Increase this if you have legit
 final fory = Fory(maxDepth: 128);
 ```
 
+### Remote schema metadata limits
+
+Compatible mode can receive remote metadata for schema evolution. These limits
+bound metadata size and accepted schema versions:
+
+```dart
+final fory = Fory(
+  maxTypeFields: 512,
+  maxTypeMetaBytes: 4096,
+  maxSchemaVersionsPerType: 10,
+  maxAverageSchemaVersionsPerType: 3,
+);
+```
+
+- `maxTypeFields` limits fields in one received struct metadata body.
+- `maxTypeMetaBytes` limits encoded body bytes in one received TypeMeta body, excluding the 8-byte
+  header and any extended-size varint.
+- `maxSchemaVersionsPerType` limits accepted remote metadata versions for one logical type.
+- `maxAverageSchemaVersionsPerType` limits the average across accepted remote types. The
+  effective global floor is `8192` schemas.
+
 ## Defaults
 
-| Option               | Default |
-| -------------------- | ------- |
-| `compatible`         | `true`  |
-| `checkStructVersion` | `false` |
-| `maxDepth`           | 256     |
+| Option                            | Default |
+| --------------------------------- | ------- |
+| `compatible`                      | `true`  |
+| `checkStructVersion`              | `false` |
+| `maxDepth`                        | 256     |
+| `maxTypeFields`                   | 512     |
+| `maxTypeMetaBytes`                | 4096    |
+| `maxSchemaVersionsPerType`        | 10      |
+| `maxAverageSchemaVersionsPerType` | 3       |
 
 ## Xlang Notes
 
@@ -105,6 +134,8 @@ Security-related configuration:
 - Register only the expected generated models before deserializing untrusted payloads.
 - Use `checkStructVersion: true` with `compatible: false` for intentional same-schema payloads.
 - Set `maxDepth` to reject unexpectedly deep payload shapes.
+- Keep the remote schema metadata limits at their defaults unless the data is not malicious and a
+  trusted peer sends larger metadata or many schema versions.
 - Prefer generated schemas and explicit field metadata over broad dynamic fields for untrusted input.
 
 ## Related Topics
diff --git a/docs/guide/go/configuration.md b/docs/guide/go/configuration.md
index eb1aae584..20d9012ae 100644
--- a/docs/guide/go/configuration.md
+++ b/docs/guide/go/configuration.md
@@ -33,12 +33,16 @@ f := fory.New(fory.WithXlang(true))
 
 Default settings:
 
-| Option     | Default | Description                                  |
-| ---------- | ------- | -------------------------------------------- |
-| TrackRef   | false   | Reference tracking disabled                  |
-| MaxDepth   | 20      | Maximum nesting depth                        |
-| IsXlang    | true    | Xlang mode enabled                           |
-| Compatible | true    | Compatible schema-evolution metadata enabled |
+| Option                          | Default | Description                                       |
+| ------------------------------- | ------- | ------------------------------------------------- |
+| TrackRef                        | false   | Reference tracking disabled                       |
+| MaxDepth                        | 20      | Maximum nesting depth                             |
+| IsXlang                         | true    | Xlang mode enabled                                |
+| Compatible                      | true    | Compatible schema-evolution metadata enabled      |
+| MaxTypeFields                   | 512     | Max fields in one received struct metadata body   |
+| MaxTypeMetaBytes                | 4096    | Max encoded bytes in one received metadata body   |
+| MaxSchemaVersionsPerType        | 10      | Max remote metadata versions for one logical type |
+| MaxAverageSchemaVersionsPerType | 3       | Average remote metadata versions across types     |
 
 ### With Options
 
@@ -47,6 +51,10 @@ f := fory.New(
     fory.WithXlang(true),
     fory.WithTrackRef(true),
     fory.WithMaxDepth(10),
+    fory.WithMaxTypeFields(512),
+    fory.WithMaxTypeMetaBytes(4096),
+    fory.WithMaxSchemaVersionsPerType(10),
+    fory.WithMaxAverageSchemaVersionsPerType(3),
 )
 ```
 
@@ -119,6 +127,40 @@ f := fory.New(fory.WithMaxDepth(30))
 - Protects against deeply nested, recursive structures or malicious data
 - Serialization fails with error when exceeded
 
+### WithMaxTypeFields
+
+Set the maximum fields accepted in one received remote struct metadata body:
+
+```go
+f := fory.New(fory.WithMaxTypeFields(512))
+```
+
+### WithMaxTypeMetaBytes
+
+Set the maximum encoded body bytes accepted for one received TypeDef body,
+excluding the 8-byte header and any extended-size varint:
+
+```go
+f := fory.New(fory.WithMaxTypeMetaBytes(4096))
+```
+
+### WithMaxSchemaVersionsPerType
+
+Set the maximum accepted remote metadata versions for one logical type:
+
+```go
+f := fory.New(fory.WithMaxSchemaVersionsPerType(10))
+```
+
+### WithMaxAverageSchemaVersionsPerType
+
+Set the average accepted remote metadata versions across accepted remote types.
+The effective global floor is `8192` schemas:
+
+```go
+f := fory.New(fory.WithMaxAverageSchemaVersionsPerType(3))
+```
+
 ### WithXlang
 
 Select the wire mode:
@@ -346,6 +388,8 @@ Security-related configuration:
 
 - Register only the expected structs before deserializing untrusted data.
 - Use `WithMaxDepth(...)` to reject unexpectedly deep payloads.
+- Keep the remote schema metadata limits at their defaults unless the data is not malicious and a
+  trusted peer sends larger metadata or many schema versions.
 - Prefer concrete struct fields over broad `any` or interface-typed fields for untrusted input.
 
 ## Related Topics
diff --git a/docs/guide/java/configuration.md b/docs/guide/java/configuration.md
index a42da9f94..7b3ce60bf 100644
--- a/docs/guide/java/configuration.md
+++ b/docs/guide/java/configuration.md
@@ -38,6 +38,10 @@ This page documents all configuration options available through `ForyBuilder`.
 | `registerGuavaTypes`                | Whether to pre-register Guava types such as `RegularImmutableMap`/`RegularImmutableList`. These types are not public API, but seem pretty stable.                                                                                                                                                                                                                                                                                                                    [...]
 | `requireClassRegistration`          | Disabling may allow unknown classes to be deserialized, potentially causing security risks.                                                                                                                                                                                                                                                                                                                                                                          [...]
 | `maxDepth`                          | Set max depth for deserialization, when depth exceeds, an exception will be thrown. This can be used to refuse deserialization DDOS attack.                                                                                                                                                                                                                                                                                                                          [...]
+| `maxTypeFields`                     | Maximum fields accepted in one received remote struct metadata body.                                                                                                                                                                                                                                                                                                                                                                                                 [...]
+| `maxTypeMetaBytes`                  | Maximum encoded body bytes accepted for one received TypeDef or TypeMeta body, excluding the 8-byte header and any extended-size varint.                                                                                                                                                                                                                                                                                                                             [...]
+| `maxSchemaVersionsPerType`          | Maximum accepted remote metadata versions for one logical type.                                                                                                                                                                                                                                                                                                                                                                                                      [...]
+| `maxAverageSchemaVersionsPerType`   | Average accepted remote metadata versions across all accepted remote types. The effective global floor is `8192` metadata entries.                                                                                                                                                                                                                                                                                                                                   [...]
 | `suppressClassRegistrationWarnings` | Whether to suppress class registration warnings. The warnings can be used for security audit, but may be annoying, this suppression will be enabled by default.                                                                                                                                                                                                                                                                                                      [...]
 | `metaShareEnabled`                  | Enables or disables meta share mode.                                                                                                                                                                                                                                                                                                                                                                                                                                 [...]
 | `scopedMetaShareEnabled`            | Scoped meta share focuses on a single serialization process. Metadata created or identified during this process is exclusive to it and is not shared with by other serializations.                                                                                                                                                                                                                                                                                   [...]
@@ -93,6 +97,11 @@ Security-related options:
 
 - `requireClassRegistration(true)` restricts deserialization to registered classes.
 - `withMaxDepth(...)` rejects unexpectedly deep object graphs.
+- `withMaxTypeFields(...)` and `withMaxTypeMetaBytes(...)` bound the field count
+  and encoded body size of one received remote metadata body.
+- `withMaxSchemaVersionsPerType(...)` and
+  `withMaxAverageSchemaVersionsPerType(...)` bound accepted remote metadata versions without
+  changing registration, dynamic loading, or schema-evolution semantics.
 - `withDeserializeUnknownClass(false)` avoids materializing unknown classes from metadata.
 - `checkJdkClassSerializable(true)` keeps the JDK serializability check for `java.*` classes.
 - Class registration warnings can be useful during security audits; use
diff --git a/docs/guide/javascript/configuration.md b/docs/guide/javascript/configuration.md
index e4a0fb3bd..058bccf4b 100644
--- a/docs/guide/javascript/configuration.md
+++ b/docs/guide/javascript/configuration.md
@@ -43,18 +43,26 @@ const fory = new Fory({
   ref: true,
   compatible: true,
   maxDepth: 100,
+  maxTypeFields: 512,
+  maxTypeMetaBytes: 4096,
+  maxSchemaVersionsPerType: 10,
+  maxAverageSchemaVersionsPerType: 3,
   hps,
 });
 ```
 
-| Option                     | Default | Description                                                                           |
-| -------------------------- | ------- | ------------------------------------------------------------------------------------- |
-| `ref`                      | `false` | Enable reference tracking for shared or circular object graphs                        |
-| `compatible`               | `true`  | Allow field additions/removals without breaking existing messages                     |
-| `maxDepth`                 | `50`    | Maximum nesting depth. Must be `>= 2`. Increase for deeply nested structures          |
-| `useSliceString`           | `false` | Optional string-reading optimization for Node.js. Leave at default unless benchmarked |
-| `hps`                      | unset   | Optional fast string helper from `@apache-fory/hps` (Node.js 20+)                     |
-| `hooks.afterCodeGenerated` | unset   | Callback to inspect the generated serializer code, useful for debugging               |
+| Option                            | Default | Description                                                                           |
+| --------------------------------- | ------- | ------------------------------------------------------------------------------------- |
+| `ref`                             | `false` | Enable reference tracking for shared or circular object graphs                        |
+| `compatible`                      | `true`  | Allow field additions/removals without breaking existing messages                     |
+| `maxDepth`                        | `50`    | Maximum nesting depth. Must be `>= 2`. Increase for deeply nested structures          |
+| `maxTypeFields`                   | `512`   | Maximum fields accepted in one received remote struct metadata body                   |
+| `maxTypeMetaBytes`                | `4096`  | Maximum encoded body bytes accepted for one received TypeMeta body                    |
+| `maxSchemaVersionsPerType`        | `10`    | Maximum accepted remote metadata versions for one logical type                        |
+| `maxAverageSchemaVersionsPerType` | `3`     | Average accepted remote metadata versions across accepted remote types                |
+| `useSliceString`                  | `false` | Optional string-reading optimization for Node.js. Leave at default unless benchmarked |
+| `hps`                             | unset   | Optional fast string helper from `@apache-fory/hps` (Node.js 20+)                     |
+| `hooks.afterCodeGenerated`        | unset   | Callback to inspect the generated serializer code, useful for debugging               |
 
 ## Reference Tracking
 
@@ -102,6 +110,11 @@ Security-related configuration:
 
 - Register only the expected schemas before deserializing untrusted payloads.
 - Set `maxDepth` for the maximum nesting depth your service accepts.
+- Keep `maxTypeFields` and `maxTypeMetaBytes` at their defaults unless the data
+  is not malicious and a trusted peer sends larger remote metadata.
+- Keep `maxSchemaVersionsPerType` and
+  `maxAverageSchemaVersionsPerType` at their defaults unless the data is not
+  malicious and a trusted peer sends many remote schema versions.
 - Prefer explicit `Type.struct(...)` schemas over `Type.any()` for untrusted input.
 - Pass `hps` only from the official package version you deploy with Fory.
 
diff --git a/docs/guide/kotlin/configuration.md b/docs/guide/kotlin/configuration.md
index 8c40d6ea6..061f14a7f 100644
--- a/docs/guide/kotlin/configuration.md
+++ b/docs/guide/kotlin/configuration.md
@@ -135,6 +135,8 @@ and any untrusted payload source:
 val fory = ForyKotlin.builder()
     .requireClassRegistration(true)
     .withMaxDepth(50)
+    .withMaxTypeFields(512)
+    .withMaxTypeMetaBytes(4096)
     .build()
 ```
 
@@ -142,5 +144,8 @@ Security-related configuration:
 
 - Keep `requireClassRegistration(true)` and register application classes or generated modules.
 - Use `withMaxDepth(...)` to reject unexpectedly deep object graphs.
+- Keep `withMaxTypeFields(...)`, `withMaxTypeMetaBytes(...)`, and the remote schema-version limits
+  at their defaults unless the data is not malicious and a trusted peer sends larger metadata or
+  many schema versions.
 - Follow [Java Configuration](../java/configuration.md#security) for allow-listing and unknown-class
   controls.
diff --git a/docs/guide/python/configuration.md b/docs/guide/python/configuration.md
index 02c61eb11..fdd6459fe 100644
--- a/docs/guide/python/configuration.md
+++ b/docs/guide/python/configuration.md
@@ -36,6 +36,10 @@ class Fory:
         strict: bool = True,
         compatible: Optional[bool] = None,
         max_depth: int = 50,
+        max_type_fields: int = 512,
+        max_type_meta_bytes: int = 4096,
+        max_schema_versions_per_type: int = 10,
+        max_average_schema_versions_per_type: int = 3,
         policy: DeserializationPolicy = None,
         field_nullable: bool = False,
         meta_compressor=None,
@@ -55,17 +59,21 @@ class ThreadSafeFory:
 
 ## Parameters
 
-| Parameter         | Type                            | Default | Description                                                                                                                                              |
-| ----------------- | ------------------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| `xlang`           | `bool`                          | `True`  | Use xlang mode. Set `False` for Python native mode.                                                                                                      |
-| `ref`             | `bool`                          | `False` | Enable reference tracking for shared/circular references. Disable for better performance if your data has no shared references.                          |
-| `strict`          | `bool`                          | `True`  | Require type registration for security. Keep this enabled for production unless a policy owns trust decisions.                                           |
-| `compatible`      | `bool \| None`                  | `None`  | Schema evolution mode. `None` enables compatible mode in both xlang and native mode. Set `False` only when every reader and writer uses the same schema. |
-| `max_depth`       | `int`                           | `50`    | Maximum deserialization depth for security, preventing stack overflow attacks.                                                                           |
-| `policy`          | `DeserializationPolicy \| None` | `None`  | Deserialization policy used for security checks. Strongly recommended when `strict=False`.                                                               |
-| `field_nullable`  | `bool`                          | `False` | Treat dataclass fields as nullable by default.                                                                                                           |
-| `meta_compressor` | `Any`                           | `None`  | Optional metadata compressor used for compatible-mode metadata encoding.                                                                                 |
-| `fory_factory`    | `Callable \| None`              | `None`  | `ThreadSafeFory` factory hook. When set, `ThreadSafeFory` creates instances via this callback; otherwise it forwards `**kwargs` to `Fory` construction.  |
+| Parameter                              | Type                            | Default | Description                                                                                                                                              |
+| -------------------------------------- | ------------------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `xlang`                                | `bool`                          | `True`  | Use xlang mode. Set `False` for Python native mode.                                                                                                      |
+| `ref`                                  | `bool`                          | `False` | Enable reference tracking for shared/circular references. Disable for better performance if your data has no shared references.                          |
+| `strict`                               | `bool`                          | `True`  | Require type registration for security. Keep this enabled for production unless a policy owns trust decisions.                                           |
+| `compatible`                           | `bool \| None`                  | `None`  | Schema evolution mode. `None` enables compatible mode in both xlang and native mode. Set `False` only when every reader and writer uses the same schema. |
+| `max_depth`                            | `int`                           | `50`    | Maximum deserialization depth for security, preventing stack overflow attacks.                                                                           |
+| `max_type_fields`                      | `int`                           | `512`   | Maximum fields accepted in one received remote struct metadata body.                                                                                     |
+| `max_type_meta_bytes`                  | `int`                           | `4096`  | Maximum encoded body bytes accepted for one received TypeDef body, excluding the 8-byte header and any extended-size varint.                             |
+| `max_schema_versions_per_type`         | `int`                           | `10`    | Maximum accepted remote metadata versions for one logical type.                                                                                          |
+| `max_average_schema_versions_per_type` | `int`                           | `3`     | Average accepted remote metadata versions across accepted remote types. The effective global floor is `8192` schemas.                                    |
+| `policy`                               | `DeserializationPolicy \| None` | `None`  | Deserialization policy used for security checks. Strongly recommended when `strict=False`.                                                               |
+| `field_nullable`                       | `bool`                          | `False` | Treat dataclass fields as nullable by default.                                                                                                           |
+| `meta_compressor`                      | `Any`                           | `None`  | Optional metadata compressor used for compatible-mode metadata encoding.                                                                                 |
+| `fory_factory`                         | `Callable \| None`              | `None`  | `ThreadSafeFory` factory hook. When set, `ThreadSafeFory` creates instances via this callback; otherwise it forwards `**kwargs` to `Fory` construction.  |
 
 ## Key Methods
 
@@ -185,6 +193,10 @@ fory = pyfory.Fory(
     ref=False,
     strict=True,
     max_depth=50,
+    max_type_fields=512,
+    max_type_meta_bytes=4096,
+    max_schema_versions_per_type=10,
+    max_average_schema_versions_per_type=3,
 )
 
 fory.register(UserModel, name="example.User")
@@ -204,6 +216,16 @@ fory = pyfory.Fory(
 )
 ```
 
+Received remote metadata is also limited:
+
+- `max_type_fields` limits the number of fields accepted in one received struct metadata body.
+- `max_type_meta_bytes` limits the encoded body bytes accepted for one received TypeDef body.
+- `max_schema_versions_per_type` limits accepted remote metadata versions for one logical type.
+- `max_average_schema_versions_per_type` limits the average across accepted remote types.
+
+These limits do not change `strict`, `policy`, dynamic loading, unknown-class handling, or
+schema-evolution semantics.
+
 ### DeserializationPolicy
 
 When `strict=False` is necessary, use `DeserializationPolicy` to restrict the dynamic types and
diff --git a/docs/guide/rust/configuration.md b/docs/guide/rust/configuration.md
index 4d93c5305..58bd07056 100644
--- a/docs/guide/rust/configuration.md
+++ b/docs/guide/rust/configuration.md
@@ -88,6 +88,28 @@ let fory = Fory::builder().max_dyn_depth(10).build(); // Allow up to 10 levels
 
 Note: Static data types (non-dynamic types) are secure by nature and not subject to depth limits, as their structure is known at compile time.
 
+### Remote Schema Metadata Limits
+
+Compatible mode can receive remote metadata for schema evolution. These limits
+bound metadata size and accepted schema versions:
+
+```rust
+let fory = Fory::builder()
+    .max_type_fields(512)
+    .max_type_meta_bytes(4096)
+    .max_schema_versions_per_type(10)
+    .max_average_schema_versions_per_type(3)
+    .build();
+```
+
+- `max_type_fields` defaults to `512` and limits fields in one received struct metadata body.
+- `max_type_meta_bytes` defaults to `4096` and limits encoded body bytes in one received TypeDef or
+  TypeMeta body, excluding the 8-byte header and any extended-size varint.
+- `max_schema_versions_per_type` defaults to `10` and limits accepted remote metadata versions for
+  one logical type.
+- `max_average_schema_versions_per_type` defaults to `3` and limits the average across accepted
+  remote types. The effective global floor is `8192` schemas.
+
 ### Explicit Xlang Examples
 
 Set `.xlang(true)` explicitly for xlang serialization examples:
@@ -122,11 +144,15 @@ let fory = Fory::builder()
 
 ## Configuration Summary
 
-| Option               | Description                             | Default |
-| -------------------- | --------------------------------------- | ------- |
-| `compatible(bool)`   | Enable schema evolution                 | `true`  |
-| `xlang(bool)`        | Use xlang mode                          | `true`  |
-| `max_dyn_depth(u32)` | Maximum nesting depth for dynamic types | `5`     |
+| Option                                        | Description                                       | Default |
+| --------------------------------------------- | ------------------------------------------------- | ------- |
+| `compatible(bool)`                            | Enable schema evolution                           | `true`  |
+| `xlang(bool)`                                 | Use xlang mode                                    | `true`  |
+| `max_dyn_depth(u32)`                          | Maximum nesting depth for dynamic types           | `5`     |
+| `max_type_fields(usize)`                      | Max fields in one received struct metadata body   | `512`   |
+| `max_type_meta_bytes(usize)`                  | Max encoded bytes in one received metadata body   | `4096`  |
+| `max_schema_versions_per_type(usize)`         | Max remote metadata versions for one logical type | `10`    |
+| `max_average_schema_versions_per_type(usize)` | Average remote metadata versions across types     | `3`     |
 
 ## Compatible Mode
 
@@ -143,6 +169,8 @@ Security-related configuration:
 - Register application structs and trait-object implementations before deserializing untrusted
   payloads.
 - Use `max_dyn_depth(...)` to reject unexpectedly deep dynamic object graphs.
+- Keep the remote schema metadata limits at their defaults unless the data is not malicious and a
+  trusted peer sends larger metadata or many schema versions.
 - Prefer concrete typed fields over `dyn Any` or broad trait-object fields for untrusted input.
 
 ## Related Topics
diff --git a/docs/guide/scala/configuration.md b/docs/guide/scala/configuration.md
index d78e648d2..397657d87 100644
--- a/docs/guide/scala/configuration.md
+++ b/docs/guide/scala/configuration.md
@@ -179,6 +179,8 @@ and any untrusted payload source:
 val fory = ForyScala.builder()
   .requireClassRegistration(true)
   .withMaxDepth(50)
+  .withMaxTypeFields(512)
+  .withMaxTypeMetaBytes(4096)
   .build()
 ```
 
@@ -186,5 +188,8 @@ Security-related configuration:
 
 - Keep `requireClassRegistration(true)` and register application classes or generated modules.
 - Use `withMaxDepth(...)` to reject unexpectedly deep object graphs.
+- Keep `withMaxTypeFields(...)`, `withMaxTypeMetaBytes(...)`, and the remote schema-version limits
+  at their defaults unless the data is not malicious and a trusted peer sends larger metadata or
+  many schema versions.
 - Follow [Java Configuration](../java/configuration.md#security) for allow-listing and unknown-class
   controls.
diff --git a/docs/guide/swift/configuration.md b/docs/guide/swift/configuration.md
index 85c1e3325..e3a047881 100644
--- a/docs/guide/swift/configuration.md
+++ b/docs/guide/swift/configuration.md
@@ -31,6 +31,10 @@ public struct Config {
   public let compatible: Bool
   public let checkClassVersion: Bool
   public let maxDepth: Int
+  public let maxTypeFields: Int
+  public let maxTypeMetaBytes: Int
+  public let maxSchemaVersionsPerType: Int
+  public let maxAverageSchemaVersionsPerType: Int
 }
 ```
 
@@ -86,10 +90,25 @@ let fory = Fory(compatible: false, checkClassVersion: true)
 
 ### Size and Depth Limits
 
-`maxDepth` bounds decoded payload nesting depth.
+`maxDepth` bounds decoded payload nesting depth. Compatible-mode remote metadata
+is also limited:
+
+- `maxTypeFields` defaults to `512` and limits fields in one received struct metadata body.
+- `maxTypeMetaBytes` defaults to `4096` and limits encoded body bytes in one received TypeMeta body,
+  excluding the 8-byte header and any extended-size varint.
+- `maxSchemaVersionsPerType` defaults to `10` and limits accepted remote metadata versions for one
+  logical type.
+- `maxAverageSchemaVersionsPerType` defaults to `3` and limits the average across accepted remote
+  types. The effective global floor is `8192` schemas.
 
 ```swift
-let fory = Fory(maxDepth: 5)
+let fory = Fory(
+  maxDepth: 5,
+  maxTypeFields: 512,
+  maxTypeMetaBytes: 4096,
+  maxSchemaVersionsPerType: 10,
+  maxAverageSchemaVersionsPerType: 3
+)
 ```
 
 ## Recommended Presets
@@ -121,3 +140,5 @@ Security-related configuration:
 - Register only the expected generated models before deserializing untrusted payloads.
 - Use `checkClassVersion` with `compatible: false` for intentional same-schema payloads.
 - Set `maxDepth` for the largest nesting depth your service accepts.
+- Keep the remote schema metadata limits at their defaults unless the data is not malicious and a
+  trusted peer sends larger metadata or many schema versions.
diff --git a/docs/guide/xlang/serialization.md b/docs/guide/xlang/serialization.md
index 338f58fcb..cdf136a35 100644
--- a/docs/guide/xlang/serialization.md
+++ b/docs/guide/xlang/serialization.md
@@ -23,6 +23,41 @@ This page demonstrates common cross-language serialization patterns. Data serial
 supported language can be deserialized in any other supported language when peers use matching type
 identity, field schema, and compatibility settings.
 
+## Remote Schema Metadata Limits
+
+Compatible mode may receive remote metadata (`TypeDef` or `TypeMeta`) for types that are not already
+known by the reader. Fory limits how many distinct remote metadata versions can be accepted, and
+also limits the size of each received metadata body:
+
+- `maxSchemaVersionsPerType`: maximum accepted remote metadata versions for one logical type. The
+  default is `10`.
+- `maxAverageSchemaVersionsPerType`: average accepted remote metadata versions across all accepted
+  remote types. The default is `3`; the effective global floor is `8192` metadata entries.
+- `maxTypeFields`: maximum fields declared by one received struct metadata body. The default is
+  `512`.
+- `maxTypeMetaBytes`: maximum encoded metadata body bytes for one received TypeDef or TypeMeta body,
+  excluding the 8-byte header and any extended-size varint. The default is `4096`.
+
+These limits are resource protections. They do not change wire format, registration requirements,
+dynamic type loading, unknown-type handling, or schema-evolution compatibility.
+
+Raise these values only when the data is not malicious and a trusted peer sends larger metadata or
+many schema versions.
+
+| Language              | Field-count option  | Metadata-bytes option  | Per-type option                | Average option                         |
+| --------------------- | ------------------- | ---------------------- | ------------------------------ | -------------------------------------- |
+| Java                  | `withMaxTypeFields` | `withMaxTypeMetaBytes` | `withMaxSchemaVersionsPerType` | `withMaxAverageSchemaVersionsPerType`  |
+| Scala                 | `withMaxTypeFields` | `withMaxTypeMetaBytes` | `withMaxSchemaVersionsPerType` | `withMaxAverageSchemaVersionsPerType`  |
+| Kotlin                | `withMaxTypeFields` | `withMaxTypeMetaBytes` | `withMaxSchemaVersionsPerType` | `withMaxAverageSchemaVersionsPerType`  |
+| Python                | `max_type_fields`   | `max_type_meta_bytes`  | `max_schema_versions_per_type` | `max_average_schema_versions_per_type` |
+| JavaScript/TypeScript | `maxTypeFields`     | `maxTypeMetaBytes`     | `maxSchemaVersionsPerType`     | `maxAverageSchemaVersionsPerType`      |
+| C++                   | `max_type_fields`   | `max_type_meta_bytes`  | `max_schema_versions_per_type` | `max_average_schema_versions_per_type` |
+| Go                    | `WithMaxTypeFields` | `WithMaxTypeMetaBytes` | `WithMaxSchemaVersionsPerType` | `WithMaxAverageSchemaVersionsPerType`  |
+| Rust                  | `max_type_fields`   | `max_type_meta_bytes`  | `max_schema_versions_per_type` | `max_average_schema_versions_per_type` |
+| C#                    | `MaxTypeFields`     | `MaxTypeMetaBytes`     | `MaxSchemaVersionsPerType`     | `MaxAverageSchemaVersionsPerType`      |
+| Swift                 | `maxTypeFields`     | `maxTypeMetaBytes`     | `maxSchemaVersionsPerType`     | `maxAverageSchemaVersionsPerType`      |
+| Dart                  | `maxTypeFields`     | `maxTypeMetaBytes`     | `maxSchemaVersionsPerType`     | `maxAverageSchemaVersionsPerType`      |
+
 ## Serialize Built-in Types
 
 Common types can be serialized automatically without registration: primitive numeric types, string, binary, array, list, map, and more.
diff --git a/docs/security/deserialization.md b/docs/security/deserialization.md
index 390b1855b..041610b29 100644
--- a/docs/security/deserialization.md
+++ b/docs/security/deserialization.md
@@ -195,8 +195,59 @@ Metadata readers should:
 - Avoid unbounded table growth from attacker-controlled metadata streams.
 - Validate metadata bodies before using them to bypass or replace existing
   policy decisions.
+- For Java metadata paths, keep name-level checks such as `TypeChecker` and the
+  disallowed-class list before `Class.forName` by routing remote class-name
+  loading through the existing `TypeResolver.loadClass` owner. Do not bypass
+  that owner with direct class loading from TypeDef or TypeMeta names. Other
+  deserialization checks that require a materialized `Class<?>`, such as
+  post-load class policy checks, remain after loading; do not move them earlier
+  or replace them with string-only approximations that change registration,
+  dynamic-loading, or unknown-type semantics.
 - Reset or release metadata state at the correct root-operation boundary.
 
+Remote metadata that can create persistent read state must be bounded before
+that state is retained. The check is resource control only: it must not change
+wire compatibility, type registration, dynamic class loading, unknown-type
+handling, deserialization policy, or schema-evolution semantics. Failed or
+incompatible metadata must not consume schema-version limits, and metadata
+cache hits or generated field readers must not add validation, hashing,
+allocation, or policy work for these limits. The concrete sequence for metadata
+parsing, cache publishing, exact-local matching, and counting belongs to the
+[xlang implementation guide](../specification/xlang_implementation_guide.md).
+
+The checked metadata cache is the only owner of whether a received TypeDef or
+TypeMeta header has already been validated. A metadata cache hit means the
+header was previously parsed, body/hash-validated, policy-checked, and
+published by the owning cache, so the reader must skip the remaining metadata
+body and use the cached metadata without repeating body validation, hash
+validation, limit checks, exact-local checks, or policy work. A metadata cache
+miss is the only path that parses the metadata body, validates its hash and
+shape, enforces metadata limits, performs exact-local byte comparison, and
+publishes to the cache. Do not add separate nullable flags, sentinel headers,
+per-TypeInfo acceptance markers, or parallel state to represent this decision.
+
+Only metadata that is actually carried as a TypeDef or TypeMeta body is subject
+to metadata body and schema-version limits. Compatible named enum, ext, and
+union metadata normally has one version, but still counts against remote
+metadata total limits when it is sent as shared metadata. Pure id-based enum,
+ext, and typed-union values use type id plus user type id and must not be moved
+onto this metadata body path.
+
+Remote metadata bodies and struct field lists must also be bounded on the cold
+metadata parse path. `maxTypeMetaBytes` limits the encoded metadata body bytes
+for one received TypeDef or TypeMeta body, excluding the 8-byte header and any
+extended-size varint. `maxTypeFields` limits the number of fields declared by
+one received struct metadata body. For Java native TypeDef class layers, the
+field limit applies to the total field count across the class layers in that
+one TypeDef. These limits are checked before copying, decompressing, reserving,
+or allocating from attacker-declared metadata sizes or field counts.
+
+The default limits are `maxTypeFields = 512` and `maxTypeMetaBytes = 4096`.
+Runtimes should report limit failures as possible malicious data and tell users
+to increase the exact option only when the data is not malicious. These limits
+must not introduce validation on metadata cache-hit, generated serializer, or
+already-resolved type-id hot paths.
+
 Metadata byte-form strictness alone is not a security requirement. Rejecting a
 metadata shape is useful only when the owner wants that strictness or when the
 shape changes type identity, retained state, resource use, or policy behavior.
diff --git a/docs/specification/java_serialization_spec.md b/docs/specification/java_serialization_spec.md
index 609bae700..607fffe77 100644
--- a/docs/specification/java_serialization_spec.md
+++ b/docs/specification/java_serialization_spec.md
@@ -410,6 +410,12 @@ Root kind codes:
 Class layers are encoded from parent to leaf. Field lists inside each layer use
 the field order defined above.
 
+Readers may reject a received TypeDef that exceeds runtime resource limits such
+as maximum metadata body bytes or maximum fields in one TypeDef. These limits
+are receive-side resource controls and do not change the TypeDef wire encoding,
+type identity, dynamic class loading, unknown-class handling, registration
+policy, or schema-evolution semantics.
+
 ### Field Info
 
 Each field is encoded as:
diff --git a/docs/specification/xlang_implementation_guide.md b/docs/specification/xlang_implementation_guide.md
index da68fd585..d8fb20501 100644
--- a/docs/specification/xlang_implementation_guide.md
+++ b/docs/specification/xlang_implementation_guide.md
@@ -22,7 +22,7 @@ license: |
 ## Overview
 
 This guide describes the current xlang implementation ownership model used by
-the reference Java implementation and mirrored by the Dart implementation rewrite.
+the xlang runtimes.
 
 The wire format is defined by
 [Xlang Serialization Spec](xlang_serialization_spec.md). This document is about
@@ -403,6 +403,57 @@ are readable through the byte owner. Field-list allocation should happen after
 that body readability check and should not use a separate small initial-capacity
 cap as a security rule.
 
+Implementations should also bound received metadata bodies and struct field
+lists on the cold metadata parse path. `maxTypeMetaBytes` limits one encoded
+TypeDef or TypeMeta body, excluding the 8-byte header and any extended-size
+varint, and is checked before copying or decompressing that body.
+`maxTypeFields` limits the number of fields declared by one received struct
+metadata body and is checked before reserving or allocating the field list.
+These limits are runtime resource controls; they do not change wire encoding,
+type identity, dynamic loading, unknown-type behavior, deserialization policy,
+or schema-evolution semantics. Metadata cache hits and generated field readers
+remain hot paths and must not add work for these limits.
+
+Remote schema-version limits belong to the same cold metadata owner path.
+Header cache hits must skip the remaining metadata body and return cached
+metadata without schema-limit checks, hash revalidation, allocation, or policy
+work. On a header miss, keep the handling in one concrete owner path: prove and
+read the metadata body bytes, validate the body against its header, validate
+field counts, resolve the type through the existing registration and
+deserialization-policy checks, compare exact local metadata by original encoded
+bytes when applicable, check schema-version limits for non-local remote
+metadata, build the required read state, publish to the persistent metadata
+cache, and then record the schema count. Failed or incompatible metadata must
+not publish to the persistent cache and must not consume schema-version counts.
+
+Remote metadata whose encoded bytes exactly match the local registered metadata
+may use the local metadata without consuming the remote schema-version limit,
+after the existing type and deserialization-policy checks for selecting that
+local type have run. This exact-local bypass is not struct-only; it also applies
+to named enum, ext, and union metadata when those metadata bodies are present and
+match the local encoded bytes. Pure id-based enum, ext, and typed-union values
+do not carry TypeDef or TypeMeta bodies and must stay on the normal type-id plus
+user-type-id path. Compatible named enum, ext, and union metadata normally has
+one version, but it still counts against accepted remote metadata totals when it
+is sent as shared metadata and does not exactly match local metadata.
+`maxTypeFields` applies only to struct field lists.
+
+The exact-local candidate must be derived inside the metadata owner path from
+the decoded metadata identity: `userTypeId` for id-registered metadata, or
+`(namespace, typeName)` for name-registered metadata. Do not thread extra
+expected-type parameters through read callers solely for this check. This rule
+applies to every runtime. Java and Python may lazy-build the local encoded
+metadata only after this identity lookup selects a local class and the existing
+class, registration, and deserialization-policy checks for that class have run.
+
+When a statically declared compatible named enum, ext, or union field reads
+shared metadata, the decoded metadata must match the declared type id,
+namespace, and type name before the metadata owner publishes it to the
+persistent cache or records a schema count. Already accepted header or reference
+cache hits still skip the body and must not rerun body-hash, schema-limit, or
+registration checks, but the field reader must not treat metadata for a
+different declared named type as the current field's metadata.
+
 Skip paths do not need to materialize skipped values. Existing byte-skip
 operations should consume any available buffered prefix first, then skip or drop
 remaining stream bytes in bounded steps.
@@ -510,7 +561,7 @@ Two explicit pieces of state back xlang type metadata:
 
 - `MetaStringWriter` and `MetaStringReader` deduplicate and decode namespace
   and type-name strings
-- shared TypeDef write/read state tracks announced compatible struct metadata
+- shared TypeDef write/read state tracks announced TypeDef metadata
 
 Ownership rules:
 
diff --git a/docs/specification/xlang_serialization_spec.md b/docs/specification/xlang_serialization_spec.md
index cf4690434..71220b773 100644
--- a/docs/specification/xlang_serialization_spec.md
+++ b/docs/specification/xlang_serialization_spec.md
@@ -651,6 +651,10 @@ After the type ID:
 - **UNION**: no extra bytes at this layer.
 - **LIST / SET / MAP / primitives**: no extra bytes at this layer.
 
+Pure id-based enum, ext, and typed-union values therefore do not carry a TypeDef
+body. Receive-side TypeDef resource limits apply only when the stream actually
+carries shared TypeDef metadata.
+
 `ARRAY (42)` is reserved for a future xlang extension for dedicated multi-dimensional arrays and
 is not used in current xlang streams.
 
@@ -743,6 +747,12 @@ Meta header byte for non-struct TypeDefs:
 - Bits 4-6: reserved (must be zero).
 - Bits 0-3: kind code.
 
+Readers may reject a received TypeDef that exceeds runtime resource limits such
+as maximum metadata body bytes or maximum fields in one struct TypeDef. These
+limits are receive-side resource controls and do not change TypeDef wire
+encoding, type identity, dynamic loading, unknown-type handling, registration
+policy, or schema-evolution semantics.
+
 Non-struct kind codes:
 
 - `0`: `ENUM`
diff --git a/go/fory/enum_test.go b/go/fory/enum_test.go
index 90fa57ddf..2d2287589 100644
--- a/go/fory/enum_test.go
+++ b/go/fory/enum_test.go
@@ -28,6 +28,39 @@ type auditEnum int32
 type otherAuditEnum int32
 type namedAuditEnum int32
 
+type idLimitExt struct {
+	ID int32
+}
+
+type idLimitExtSerializer struct{}
+
+func (s *idLimitExtSerializer) WriteData(ctx *WriteContext, value reflect.Value) {
+	if value.Kind() == reflect.Ptr {
+		value = value.Elem()
+	}
+	ctx.Buffer().WriteVarint32(value.Interface().(idLimitExt).ID)
+}
+
+func (s *idLimitExtSerializer) ReadData(ctx *ReadContext, value reflect.Value) {
+	value.Set(reflect.ValueOf(idLimitExt{ID: ctx.Buffer().ReadVarint32(ctx.Err())}))
+}
+
+type idLimitUnion struct {
+	caseID uint32
+	value  any
+}
+
+func (idLimitUnion) ForyUnionMarker() {}
+
+func (u idLimitUnion) ForyUnionGet() (uint32, any) {
+	return u.caseID, u.value
+}
+
+func (u *idLimitUnion) ForyUnionSet(caseID uint32, value any) {
+	u.caseID = caseID
+	u.value = value
+}
+
 func TestEnumReadConsumesRegisteredTypeInfo(t *testing.T) {
 	f := NewFory(WithXlang(true), WithCompatible(false))
 	require.NoError(t, f.RegisterEnum(auditEnum(0), 101))
@@ -51,6 +84,70 @@ func TestEnumReadConsumesRegisteredTypeInfo(t *testing.T) {
 	require.Equal(t, buf.WriterIndex(), f.readCtx.Buffer().ReaderIndex())
 }
 
+func TestIdEnumDoesNotUseTypeDefMeta(t *testing.T) {
+	f := NewFory(
+		WithXlang(true),
+		WithCompatible(true),
+		WithMaxTypeMetaBytes(1),
+		WithMaxSchemaVersionsPerType(1),
+	)
+	require.NoError(t, f.RegisterEnum(auditEnum(0), 101))
+	enumType := reflect.TypeOf(auditEnum(0))
+	serializer, err := f.typeResolver.getSerializerByType(enumType, false)
+	require.NoError(t, err)
+	typeInfo := f.typeResolver.typesInfo[enumType]
+	require.NotNil(t, typeInfo)
+
+	buf := NewByteBuffer(nil)
+	bufErr := &Error{}
+	f.typeResolver.WriteTypeInfo(buf, typeInfo, bufErr)
+	require.NoError(t, bufErr.CheckError())
+	buf.WriteVarUint32Small7(2)
+
+	f.readCtx.SetData(buf.Bytes())
+	var result auditEnum
+	serializer.Read(f.readCtx, RefModeNone, true, false, reflect.ValueOf(&result).Elem())
+	require.NoError(t, f.readCtx.CheckError())
+	require.Equal(t, auditEnum(2), result)
+	require.Equal(t, buf.WriterIndex(), f.readCtx.Buffer().ReaderIndex())
+}
+
+func TestIdExtDoesNotUseTypeDefMeta(t *testing.T) {
+	f := NewFory(
+		WithXlang(true),
+		WithCompatible(true),
+		WithMaxTypeMetaBytes(1),
+		WithMaxSchemaVersionsPerType(1),
+	)
+	require.NoError(t, f.RegisterExtension(idLimitExt{}, 102, &idLimitExtSerializer{}))
+
+	data, err := f.Serialize(&idLimitExt{ID: 42})
+	require.NoError(t, err)
+	var result any
+	require.NoError(t, f.Deserialize(data, &result))
+	require.Equal(t, idLimitExt{ID: 42}, result)
+}
+
+func TestIdUnionDoesNotUseTypeDefMeta(t *testing.T) {
+	f := NewFory(
+		WithXlang(true),
+		WithCompatible(true),
+		WithMaxTypeMetaBytes(1),
+		WithMaxSchemaVersionsPerType(1),
+	)
+	require.NoError(t, f.RegisterUnion(
+		idLimitUnion{},
+		103,
+		NewUnionSerializer(UnionCase{ID: 0, Type: reflect.TypeOf("")}),
+	))
+
+	data, err := f.Serialize(&idLimitUnion{caseID: 0, value: "hello"})
+	require.NoError(t, err)
+	var result any
+	require.NoError(t, f.Deserialize(data, &result))
+	require.Equal(t, idLimitUnion{caseID: 0, value: "hello"}, result)
+}
+
 func TestEnumReadRejectsMismatchedRegisteredTypeInfo(t *testing.T) {
 	f := NewFory(WithXlang(true), WithCompatible(false))
 	require.NoError(t, f.RegisterEnum(auditEnum(0), 101))
diff --git a/go/fory/fory.go b/go/fory/fory.go
index 964d72c85..412fc4644 100644
--- a/go/fory/fory.go
+++ b/go/fory/fory.go
@@ -65,20 +65,26 @@ const (
 
 // Config holds configuration options for Fory instances
 type Config struct {
-	TrackRef      bool
-	MaxDepth      int
-	IsXlang       bool
-	Compatible    bool // Schema evolution compatibility mode
-	MaxTypeFields int
+	TrackRef                        bool
+	MaxDepth                        int
+	IsXlang                         bool
+	Compatible                      bool // Schema evolution compatibility mode
+	MaxTypeFields                   int
+	MaxTypeMetaBytes                int
+	MaxSchemaVersionsPerType        int
+	MaxAverageSchemaVersionsPerType int
 }
 
 // defaultConfig returns the default configuration
 func defaultConfig() Config {
 	return Config{
-		TrackRef:      false, // Match Java's default: reference tracking disabled
-		MaxDepth:      20,
-		IsXlang:       true,
-		MaxTypeFields: 10000,
+		TrackRef:                        false, // Match Java's default: reference tracking disabled
+		MaxDepth:                        20,
+		IsXlang:                         true,
+		MaxTypeFields:                   512,
+		MaxTypeMetaBytes:                4096,
+		MaxSchemaVersionsPerType:        10,
+		MaxAverageSchemaVersionsPerType: 3,
 	}
 }
 
@@ -121,11 +127,44 @@ func WithCompatible(enabled bool) Option {
 
 // WithMaxTypeFields sets the maximum field count limit for schema definition deserialization
 func WithMaxTypeFields(size int) Option {
+	if size <= 0 {
+		panic("MaxTypeFields must be positive")
+	}
 	return func(f *Fory) {
 		f.config.MaxTypeFields = size
 	}
 }
 
+// WithMaxTypeMetaBytes sets the maximum body size for received type metadata.
+func WithMaxTypeMetaBytes(size int) Option {
+	if size <= 0 {
+		panic("MaxTypeMetaBytes must be positive")
+	}
+	return func(f *Fory) {
+		f.config.MaxTypeMetaBytes = size
+	}
+}
+
+// WithMaxSchemaVersionsPerType sets the maximum accepted remote metadata versions for one logical type.
+func WithMaxSchemaVersionsPerType(size int) Option {
+	if size <= 0 {
+		panic("MaxSchemaVersionsPerType must be positive")
+	}
+	return func(f *Fory) {
+		f.config.MaxSchemaVersionsPerType = size
+	}
+}
+
+// WithMaxAverageSchemaVersionsPerType sets the average remote metadata version limit across accepted remote types.
+func WithMaxAverageSchemaVersionsPerType(size int) Option {
+	if size <= 0 {
+		panic("MaxAverageSchemaVersionsPerType must be positive")
+	}
+	return func(f *Fory) {
+		f.config.MaxAverageSchemaVersionsPerType = size
+	}
+}
+
 // ============================================================================
 // Fory - Main serialization instance
 // ============================================================================
diff --git a/go/fory/type_def.go b/go/fory/type_def.go
index f654e7b8f..8b303123d 100644
--- a/go/fory/type_def.go
+++ b/go/fory/type_def.go
@@ -286,7 +286,8 @@ func readTypeDef(fory *Fory, buffer *ByteBuffer, header int64, err *Error) *Type
 func skipTypeDef(buffer *ByteBuffer, header int64, err *Error) {
 	// Header-cache hits intentionally treat the current body as opaque bytes and skip by the size in
 	// the current header. Parsed TypeDefs are published to the cache only after successful body parse
-	// and 52-bit metadata-hash validation; cache hits must not reparse or rehash that body.
+	// and 52-bit metadata-hash validation; cache hits must not reparse, rehash, allocate, or
+	// otherwise materialize that body.
 	sz := int(header & META_SIZE_MASK)
 	if sz == META_SIZE_MASK {
 		sz += int(buffer.ReadVarUint32(err))
@@ -396,6 +397,7 @@ func buildTypeDef(fory *Fory, value reflect.Value) (*TypeDef, error) {
 	}
 	registerByName := IsNamespacedType(TypeId(typeId))
 	typeDef := NewTypeDef(typeId, infoPtr.UserTypeID, infoPtr.PkgPathBytes, infoPtr.NameBytes, registerByName, false, fieldDefs)
+	typeDef.type_ = value.Type()
 
 	// encoding the typeDef, and save the encoded bytes
 	encoded, err := encodingTypeDef(fory.typeResolver, typeDef)
@@ -1015,7 +1017,14 @@ func decodeTypeDef(fory *Fory, buffer *ByteBuffer, header int64) (*TypeDef, erro
 		extraMetaSize = int(extra)
 		metaSize += extraMetaSize
 	}
-	// Store the encoded bytes for the TypeDef (including meta header and metadata)
+	if metaSize > fory.config.MaxTypeMetaBytes {
+		return nil, fmt.Errorf(
+			"type metadata body size %d exceeds MaxTypeMetaBytes %d. The data may be malicious. If the data is not malicious, please increase MaxTypeMetaBytes",
+			metaSize, fory.config.MaxTypeMetaBytes)
+	}
+	// Store the encoded bytes only through ReadBinary, which checks/fills the declared
+	// body bytes before returning a slice or allocating. MaxTypeMetaBytes is a size cap,
+	// not proof that the input contains metaSize bytes.
 	encodedMeta := buffer.ReadBinary(metaSize, &bufErr)
 	if bufErr.HasError() {
 		return nil, bufErr.TakeError()
@@ -1043,8 +1052,13 @@ func decodeTypeDef(fory *Fory, buffer *ByteBuffer, header int64) (*TypeDef, erro
 		if metaErr.HasError() {
 			return nil, metaErr.TakeError()
 		}
-		if fieldCount > fory.config.MaxTypeFields || fieldCount > metaBuffer.remaining() {
-			return nil, fmt.Errorf("field count exceeds maximum allowed limit or available buffer size")
+		if fieldCount > fory.config.MaxTypeFields {
+			return nil, fmt.Errorf(
+				"type metadata field count %d exceeds MaxTypeFields %d. The data may be malicious. If the data is not malicious, please increase MaxTypeFields",
+				fieldCount, fory.config.MaxTypeFields)
+		}
+		if fieldCount > metaBuffer.remaining() {
+			return nil, fmt.Errorf("type metadata field count exceeds available buffer size")
 		}
 		if registeredByName {
 			if (metaHeaderByte & CompatibleTypeDefFlag) != 0 {
@@ -1172,9 +1186,7 @@ func decodeTypeDef(fory *Fory, buffer *ByteBuffer, header int64) (*TypeDef, erro
 			if fallbackInfo, fallbackExists := fory.typeResolver.namedTypeToTypeInfo[nameKey]; fallbackExists {
 				info = fallbackInfo
 				exists = true
-				if len(fory.typeResolver.nsTypeToTypeInfo) < maxCachedNamedTypeInfos {
-					fory.typeResolver.nsTypeToTypeInfo[nsTypeKey{nsBytes.Hashcode, nameBytes.Hashcode}] = info
-				}
+				fory.typeResolver.nsTypeToTypeInfo[nsTypeKey{nsBytes.Hashcode, nameBytes.Hashcode}] = info
 			}
 		}
 		if exists {
@@ -1216,12 +1228,12 @@ func decodeTypeDef(fory *Fory, buffer *ByteBuffer, header int64) (*TypeDef, erro
 		}
 		fieldInfos[i] = fieldInfo
 	}
-	if !isStruct && len(fieldInfos) != 0 {
-		return nil, fmt.Errorf("non-struct TypeDef cannot carry field metadata")
-	}
 	if metaErr.HasError() {
 		return nil, metaErr.TakeError()
 	}
+	if !isStruct && len(fieldInfos) != 0 {
+		return nil, fmt.Errorf("non-struct TypeDef cannot carry field metadata")
+	}
 	if remaining := metaBuffer.remaining(); remaining != 0 {
 		return nil, fmt.Errorf("TypeDef metadata body has %d trailing bytes", remaining)
 	}
diff --git a/go/fory/type_def_test.go b/go/fory/type_def_test.go
index a3f1bb0cc..c395bc2d3 100644
--- a/go/fory/type_def_test.go
+++ b/go/fory/type_def_test.go
@@ -38,6 +38,32 @@ type SliceStruct struct {
 	Items []string
 }
 
+type SchemaLimitBad struct {
+	Value string
+}
+
+type SchemaLimitExtra struct {
+	Extra int32
+}
+
+type lateTypeDefExt struct {
+	Value int32
+}
+
+type lateTypeDefHolder struct {
+	Value lateTypeDefExt
+}
+
+type lateTypeDefExtSerializer struct{}
+
+func (lateTypeDefExtSerializer) WriteData(ctx *WriteContext, value reflect.Value) {
+	ctx.Buffer().WriteInt32(int32(value.FieldByName("Value").Int()))
+}
+
+func (lateTypeDefExtSerializer) ReadData(ctx *ReadContext, value reflect.Value) {
+	value.FieldByName("Value").SetInt(int64(ctx.Buffer().ReadInt32(ctx.Err())))
+}
+
 type NestedSliceStruct struct {
 	ID      int32
 	Matrix  [][]int
@@ -143,6 +169,17 @@ func TestTypeDefEncodingDecoding(t *testing.T) {
 	}
 }
 
+func TestTypeDefUsesLateRegisteredFieldType(t *testing.T) {
+	fory := NewFory(WithXlang(false), WithCompatible(true))
+	require.NoError(t, fory.RegisterStructByName(lateTypeDefHolder{}, "example.LateTypeDefHolder"))
+	require.NoError(t, fory.RegisterExtensionByName(lateTypeDefExt{}, "example.LateTypeDefExt", lateTypeDefExtSerializer{}))
+
+	typeDef, err := fory.typeResolver.getTypeDef(reflect.TypeOf(lateTypeDefHolder{}), true)
+	require.NoError(t, err)
+	require.Len(t, typeDef.fieldDefs, 1)
+	require.Equal(t, TypeId(NAMED_EXT), typeDef.fieldDefs[0].typeSpec.TypeId())
+}
+
 func checkFieldDef(t *testing.T, original, decoded FieldDef) {
 	assert.Equal(t, original.name, decoded.name, "Field name mismatch")
 	assert.Equal(t, original.nameEncoding, decoded.nameEncoding, "Field name encoding mismatch")
@@ -345,6 +382,38 @@ func TestTypeDefFieldCountOOMPanic(t *testing.T) {
 	}
 }
 
+func TestTypeDefRejectsMaxTypeFields(t *testing.T) {
+	writer := NewFory(WithXlang(false), WithCompatible(true))
+	require.NoError(t, writer.RegisterStructByName(SimpleStruct{}, "example.SimpleStruct"))
+	typeDef, err := buildTypeDef(writer, reflect.ValueOf(SimpleStruct{}))
+	require.NoError(t, err)
+	buffer := NewByteBuffer(typeDef.encoded)
+	readErr := &Error{}
+	header := buffer.ReadInt64(readErr)
+	require.NoError(t, readErr.CheckError())
+
+	reader := NewFory(WithXlang(false), WithCompatible(true), WithMaxTypeFields(1))
+	_, err = decodeTypeDef(reader, buffer, header)
+	require.Error(t, err)
+	require.Contains(t, err.Error(), "MaxTypeFields")
+}
+
+func TestTypeDefRejectsMaxTypeMetaBytes(t *testing.T) {
+	writer := NewFory(WithXlang(false), WithCompatible(true))
+	require.NoError(t, writer.RegisterStructByName(SimpleStruct{}, "example.SimpleStruct"))
+	typeDef, err := buildTypeDef(writer, reflect.ValueOf(SimpleStruct{}))
+	require.NoError(t, err)
+	buffer := NewByteBuffer(typeDef.encoded)
+	readErr := &Error{}
+	header := buffer.ReadInt64(readErr)
+	require.NoError(t, readErr.CheckError())
+
+	reader := NewFory(WithXlang(false), WithCompatible(true), WithMaxTypeMetaBytes(1))
+	_, err = decodeTypeDef(reader, buffer, header)
+	require.Error(t, err)
+	require.Contains(t, err.Error(), "MaxTypeMetaBytes")
+}
+
 func TestTypeDefRejectsReservedGlobalHeaderBits(t *testing.T) {
 	fory := NewFory(WithXlang(false), WithCompatible(false))
 	buffer := NewByteBuffer(nil)
@@ -448,16 +517,13 @@ func TestTypeDefRejectsCompressedMetadata(t *testing.T) {
 	require.Contains(t, err.Error(), "compressed xlang TypeDef")
 }
 
-func TestReadSharedTypeMetaCapsParsedTypeDefCache(t *testing.T) {
+func TestReadSharedTypeMetaExactLocalPopulatesCache(t *testing.T) {
 	fory := NewFory(WithXlang(false), WithCompatible(true))
 	require.NoError(t, fory.RegisterStructByName(SimpleStruct{}, "example.SimpleStruct"))
 	typeDef, err := buildTypeDef(fory, reflect.ValueOf(SimpleStruct{}))
 	require.NoError(t, err)
 	require.NotEmpty(t, typeDef.encoded)
 
-	for i := 0; i < maxCachedTypeDefs; i++ {
-		fory.typeResolver.defIdToTypeDef[int64(i)] = typeDef
-	}
 	headerErr := &Error{}
 	header := NewByteBuffer(typeDef.encoded).ReadInt64(headerErr)
 	require.NoError(t, headerErr.CheckError())
@@ -470,11 +536,114 @@ func TestReadSharedTypeMetaCapsParsedTypeDefCache(t *testing.T) {
 	typeInfo := fory.typeResolver.readSharedTypeMeta(buffer, readErr)
 	require.NoError(t, readErr.CheckError())
 	require.NotNil(t, typeInfo)
-	require.Len(t, fory.typeResolver.defIdToTypeDef, maxCachedTypeDefs)
-	require.NotContains(t, fory.typeResolver.defIdToTypeDef, header)
+	require.Contains(t, fory.typeResolver.defIdToTypeDef, header)
+	require.Zero(t, fory.typeResolver.totalAcceptedSchemaVersions)
+
+	invalidBody := append([]byte(nil), typeDef.encoded...)
+	invalidBody[len(invalidBody)-1] ^= 1
+	buffer = NewByteBuffer(nil)
+	buffer.WriteVarUint32(0)
+	buffer.WriteBinary(invalidBody)
+	readErr = &Error{}
+	typeInfo = fory.typeResolver.readSharedTypeMeta(buffer, readErr)
+	require.NoError(t, readErr.CheckError())
+	require.NotNil(t, typeInfo)
 }
 
-func TestDecodeTypeDefFallbackNamedTypeCacheRespectsCap(t *testing.T) {
+func TestRemoteSchemaLimitRejectsExtraVersions(t *testing.T) {
+	fory := NewFory(WithXlang(false), WithCompatible(true), WithMaxSchemaVersionsPerType(1))
+	first := remoteSchemaLimitTypeDef(t, SimpleStruct{}, "example.Shared")
+	second := remoteSchemaLimitTypeDef(t, SliceStruct{}, "example.Shared")
+
+	require.NoError(t, readRemoteTypeDef(t, fory, first))
+	err := readRemoteTypeDef(t, fory, second)
+
+	require.Error(t, err)
+	require.Contains(t, err.Error(), "MaxSchemaVersionsPerType")
+}
+
+func TestRemoteSchemaLimitKeepsUnknownTypesSeparate(t *testing.T) {
+	fory := NewFory(WithXlang(false), WithCompatible(true), WithMaxSchemaVersionsPerType(1))
+	first := remoteSchemaLimitTypeDef(t, SimpleStruct{}, "example.UnknownA")
+	second := remoteSchemaLimitTypeDef(t, SliceStruct{}, "example.UnknownB")
+
+	require.NoError(t, readRemoteTypeDef(t, fory, first))
+	require.NoError(t, readRemoteTypeDef(t, fory, second))
+}
+
+func TestRemoteSchemaCheckDoesNotConsumeLimit(t *testing.T) {
+	fory := NewFory(WithXlang(false), WithCompatible(true), WithMaxSchemaVersionsPerType(1))
+	checked := remoteSchemaLimitTypeDef(t, SchemaLimitBad{}, "example.Accepted")
+	valid := remoteSchemaLimitTypeDef(t, SchemaLimitExtra{}, "example.Accepted")
+
+	typeKey, err := fory.typeResolver.checkRemoteTypeDefLimit(checked)
+	require.NoError(t, err)
+	require.NotNil(t, typeKey)
+	require.NoError(t, readRemoteTypeDef(t, fory, valid))
+}
+
+func TestRemoteNonStructTypeDefUsesLimit(t *testing.T) {
+	fory := NewFory(WithXlang(false), WithCompatible(true), WithMaxSchemaVersionsPerType(1))
+	first := remoteNamedEnumTypeDef(t, namedAuditEnum(0), "example.RemoteEnum")
+	second := NewTypeDef(uint32(NAMED_EXT), 0, first.nsName, first.typeName, true, false, nil)
+
+	typeKey, err := fory.typeResolver.checkRemoteTypeDefLimit(first)
+	require.NoError(t, err)
+	fory.typeResolver.recordRemoteTypeDef(typeKey)
+
+	_, err = fory.typeResolver.checkRemoteTypeDefLimit(second)
+	require.Error(t, err)
+	require.Contains(t, err.Error(), "MaxSchemaVersionsPerType")
+}
+
+func TestExactLocalNonStructTypeDefBypassesLimit(t *testing.T) {
+	fory := NewFory(WithXlang(true), WithCompatible(true), WithMaxSchemaVersionsPerType(1))
+	require.NoError(t, fory.RegisterEnumByName(namedAuditEnum(0), "example.RemoteEnum"))
+	exact, err := buildTypeDef(fory, reflect.ValueOf(namedAuditEnum(0)))
+	require.NoError(t, err)
+	require.NoError(t, readRemoteTypeDef(t, fory, exact))
+
+	second := NewTypeDef(uint32(NAMED_EXT), 0, exact.nsName, exact.typeName, true, false, nil)
+	_, err = fory.typeResolver.checkRemoteTypeDefLimit(second)
+	require.NoError(t, err)
+}
+
+func remoteSchemaLimitTypeDef(t *testing.T, value any, name string) *TypeDef {
+	t.Helper()
+	sender := NewFory(WithXlang(false), WithCompatible(true))
+	require.NoError(t, sender.RegisterStructByName(value, name))
+	typeDef, err := buildTypeDef(sender, reflect.ValueOf(value))
+	require.NoError(t, err)
+	return typeDef
+}
+
+func remoteNamedEnumTypeDef(t *testing.T, value any, name string) *TypeDef {
+	t.Helper()
+	sender := NewFory(WithXlang(true), WithCompatible(true))
+	require.NoError(t, sender.RegisterEnumByName(value, name))
+	typeDef, err := buildTypeDef(sender, reflect.ValueOf(value))
+	require.NoError(t, err)
+	return typeDef
+}
+
+func readRemoteTypeDef(t *testing.T, fory *Fory, typeDef *TypeDef) error {
+	t.Helper()
+	buffer := NewByteBuffer(nil)
+	buffer.WriteVarUint32(0)
+	writeErr := &Error{}
+	typeDef.writeTypeDef(buffer, writeErr)
+	require.NoError(t, writeErr.CheckError())
+
+	readErr := &Error{}
+	typeInfo := fory.typeResolver.readSharedTypeMeta(buffer, readErr)
+	if err := readErr.CheckError(); err != nil {
+		return err
+	}
+	require.NotNil(t, typeInfo)
+	return nil
+}
+
+func TestDecodeTypeDefFallbackNamedTypeCachesLookup(t *testing.T) {
 	fory := NewFory(WithXlang(false), WithCompatible(true))
 	require.NoError(t, fory.RegisterStructByName(SimpleStruct{}, "example.SimpleStruct"))
 	typeDef, err := buildTypeDef(fory, reflect.ValueOf(SimpleStruct{}))
@@ -486,9 +655,6 @@ func TestDecodeTypeDefFallbackNamedTypeCacheRespectsCap(t *testing.T) {
 	delete(fory.typeResolver.nsTypeToTypeInfo, nameKey)
 	info := fory.typeResolver.namedTypeToTypeInfo[[2]string{"example", "SimpleStruct"}]
 	require.NotNil(t, info)
-	for i := 0; len(fory.typeResolver.nsTypeToTypeInfo) < maxCachedNamedTypeInfos; i++ {
-		fory.typeResolver.nsTypeToTypeInfo[nsTypeKey{int64(i + 1), int64(i + 2)}] = info
-	}
 	require.NotContains(t, fory.typeResolver.nsTypeToTypeInfo, nameKey)
 
 	buffer := NewByteBuffer(nil)
@@ -500,8 +666,7 @@ func TestDecodeTypeDefFallbackNamedTypeCacheRespectsCap(t *testing.T) {
 	decoded := readTypeDef(fory, buffer, header, readErr)
 	require.NoError(t, readErr.CheckError())
 	require.NotNil(t, decoded)
-	require.Len(t, fory.typeResolver.nsTypeToTypeInfo, maxCachedNamedTypeInfos)
-	require.NotContains(t, fory.typeResolver.nsTypeToTypeInfo, nameKey)
+	require.Contains(t, fory.typeResolver.nsTypeToTypeInfo, nameKey)
 }
 
 func TestTypeDefRejectsNamespaceLengthBeyondMetadata(t *testing.T) {
diff --git a/go/fory/type_resolver.go b/go/fory/type_resolver.go
index 6d908b22c..f0966b059 100644
--- a/go/fory/type_resolver.go
+++ b/go/fory/type_resolver.go
@@ -18,6 +18,7 @@
 package fory
 
 import (
+	"bytes"
 	"errors"
 	"fmt"
 	"hash/fnv"
@@ -52,11 +53,10 @@ const (
 	useStringId             = 1
 	SMALL_STRING_THRESHOLD  = 16
 	// 0xffffffff is reserved for "unset".
-	maxUserTypeID           uint32 = 0xfffffffe
-	invalidUserTypeID       uint32 = 0xffffffff
-	internalTypeIDLimit            = 0xFF
-	maxCachedTypeDefs              = 8192
-	maxCachedNamedTypeInfos        = 8192
+	maxUserTypeID         uint32 = 0xfffffffe
+	invalidUserTypeID     uint32 = 0xffffffff
+	internalTypeIDLimit          = 0xFF
+	minRemoteTypeDefLimit        = 8192
 )
 
 var (
@@ -208,8 +208,10 @@ type TypeResolver struct {
 	typeNameDecoder  *meta.Decoder
 
 	// meta share related
-	typeToTypeDef  map[reflect.Type]*TypeDef
-	defIdToTypeDef map[int64]*TypeDef
+	typeToTypeDef               map[reflect.Type]*TypeDef
+	defIdToTypeDef              map[int64]*TypeDef
+	remoteSchemaVersionsByType  map[any]int
+	totalAcceptedSchemaVersions int
 
 	// Fast type cache for O(1) lookup using type pointer
 	typePointerCache map[uintptr]*TypeInfo
@@ -252,10 +254,11 @@ func newTypeResolver(fory *Fory) *TypeResolver {
 		typeNameEncoder:  meta.NewEncoder('$', '_'),
 		typeNameDecoder:  meta.NewDecoder('$', '_'),
 
-		typeToTypeDef:    make(map[reflect.Type]*TypeDef),
-		defIdToTypeDef:   make(map[int64]*TypeDef),
-		typePointerCache: make(map[uintptr]*TypeInfo),
-		unionTypeCache:   make(map[reflect.Type]bool),
+		typeToTypeDef:              make(map[reflect.Type]*TypeDef),
+		defIdToTypeDef:             make(map[int64]*TypeDef),
+		remoteSchemaVersionsByType: make(map[any]int),
+		typePointerCache:           make(map[uintptr]*TypeInfo),
+		unionTypeCache:             make(map[reflect.Type]bool),
 	}
 	// base type info for encode/decode types.
 	// composite types info will be constructed dynamically.
@@ -1414,30 +1417,14 @@ func (r *TypeResolver) writeSharedTypeMeta(buffer *ByteBuffer, typeInfo *TypeInf
 	context := r.fory.MetaContext()
 	key := typePointer(typeInfo.Type)
 	writeTypeDefInline := func() {
-		// Only build TypeDef for struct types - enums don't have field definitions
-		actualType := typeInfo.Type
-		if actualType.Kind() == reflect.Ptr {
-			actualType = actualType.Elem()
-		}
-		if actualType.Kind() == reflect.Struct {
-			typeDef, typeDefErr := r.getTypeDef(typeInfo.Type, true)
-			if typeDefErr != nil {
-				err.SetError(typeDefErr)
-				return
-			}
-			// Write TypeDef bytes inline
-			typeDef.writeTypeDef(buffer, err)
+		typeDef, typeDefErr := r.getTypeDef(typeInfo.Type, true)
+		if typeDefErr != nil {
+			err.SetError(typeDefErr)
+			return
 		}
+		typeDef.writeTypeDef(buffer, err)
 	}
 	writeTypeDefWithZeroMarker := func() {
-		actualType := typeInfo.Type
-		if actualType.Kind() == reflect.Ptr {
-			actualType = actualType.Elem()
-		}
-		if actualType.Kind() != reflect.Struct {
-			buffer.WriteUint8(0)
-			return
-		}
 		typeDef, typeDefErr := r.getTypeDef(typeInfo.Type, true)
 		if typeDefErr != nil {
 			err.SetError(typeDefErr)
@@ -1519,6 +1506,53 @@ func (r *TypeResolver) getTypeDef(typ reflect.Type, create bool) (*TypeDef, erro
 	return typeDef, nil
 }
 
+//go:noinline
+func (r *TypeResolver) checkRemoteTypeDefLimit(td *TypeDef) (any, error) {
+	var typeKey any
+	if td.registerByName {
+		if td.nsName == nil || td.typeName == nil {
+			return nil, fmt.Errorf("named remote metadata is missing namespace or type name")
+		}
+		namespace, err := r.namespaceDecoder.Decode(td.nsName.Data, td.nsName.Encoding)
+		if err != nil {
+			return nil, err
+		}
+		typeName, err := r.typeNameDecoder.Decode(td.typeName.Data, td.typeName.Encoding)
+		if err != nil {
+			return nil, err
+		}
+		typeKey = namespace + "\x00" + typeName
+	} else {
+		typeKey = td.userTypeId
+	}
+	versionsForType := r.remoteSchemaVersionsByType[typeKey]
+	if versionsForType >= r.fory.config.MaxSchemaVersionsPerType {
+		return nil, fmt.Errorf(
+			"remote schema version limit exceeded for type %v: %d >= %d. The data may be malicious. If the data is not malicious, please increase MaxSchemaVersionsPerType",
+			typeKey, versionsForType, r.fory.config.MaxSchemaVersionsPerType)
+	}
+	acceptedTypeCount := len(r.remoteSchemaVersionsByType)
+	if versionsForType == 0 {
+		acceptedTypeCount++
+	}
+	globalLimit := acceptedTypeCount * r.fory.config.MaxAverageSchemaVersionsPerType
+	if globalLimit < minRemoteTypeDefLimit {
+		globalLimit = minRemoteTypeDefLimit
+	}
+	if r.totalAcceptedSchemaVersions >= globalLimit {
+		return nil, fmt.Errorf(
+			"remote schema version limit exceeded: %d metadata versions for %d accepted remote types exceeds the average limit %d. The data may be malicious. If the data is not malicious, please increase MaxAverageSchemaVersionsPerType",
+			r.totalAcceptedSchemaVersions, acceptedTypeCount, r.fory.config.MaxAverageSchemaVersionsPerType)
+	}
+	return typeKey, nil
+}
+
+func (r *TypeResolver) recordRemoteTypeDef(typeKey any) {
+	versionsForType := r.remoteSchemaVersionsByType[typeKey]
+	r.remoteSchemaVersionsByType[typeKey] = versionsForType + 1
+	r.totalAcceptedSchemaVersions++
+}
+
 func (r *TypeResolver) readSharedTypeMeta(buffer *ByteBuffer, err *Error) *TypeInfo {
 	context := r.fory.MetaContext()
 	if context == nil {
@@ -1561,15 +1595,12 @@ func (r *TypeResolver) readSharedTypeMeta(buffer *ByteBuffer, err *Error) *TypeI
 	var td *TypeDef
 	if existingTd, exists := r.defIdToTypeDef[id]; exists {
 		// Header-cache hits intentionally skip without rehashing. Entries reach this cache only
-		// after a successful TypeDef parse and 52-bit metadata-hash validation.
+		// after a successful TypeDef parse and 52-bit metadata-hash validation. Do not add
+		// body/hash/schema-limit/exact-local checks here; the miss path owns them before publish.
 		skipTypeDef(buffer, id, err)
 		td = existingTd
 	} else {
-		newTd := readTypeDef(r.fory, buffer, id, err)
-		if err.HasError() {
-			return nil
-		}
-		td = newTd
+		return r.readTypeDefInfo(buffer, id, context, err)
 	}
 
 	typeInfo, typeInfoErr := td.getOrBuildTypeInfo(r)
@@ -1577,14 +1608,83 @@ func (r *TypeResolver) readSharedTypeMeta(buffer *ByteBuffer, err *Error) *TypeI
 		err.SetError(typeInfoErr)
 		return nil
 	}
-	if _, exists := r.defIdToTypeDef[id]; !exists && len(r.defIdToTypeDef) < maxCachedTypeDefs {
-		r.defIdToTypeDef[id] = td
-	}
+	context.readTypeInfos = append(context.readTypeInfos, typeInfo)
+	return typeInfo
+}
 
+//go:noinline
+func (r *TypeResolver) readTypeDefInfo(buffer *ByteBuffer, id int64, context *MetaContext, err *Error) *TypeInfo {
+	td := readTypeDef(r.fory, buffer, id, err)
+	if err.HasError() {
+		return nil
+	}
+	if typeInfo := r.localTypeInfoForTypeDef(td); typeInfo != nil {
+		localTd, localErr := r.localTypeDef(typeInfo)
+		if localErr != nil {
+			err.SetError(localErr)
+			return nil
+		}
+		if localTd != nil && bytes.Equal(localTd.encoded, td.encoded) {
+			r.defIdToTypeDef[id] = localTd
+			context.readTypeInfos = append(context.readTypeInfos, typeInfo)
+			return typeInfo
+		}
+	}
+	typeKey, limitErr := r.checkRemoteTypeDefLimit(td)
+	if limitErr != nil {
+		err.SetError(limitErr)
+		return nil
+	}
+	typeInfo, typeInfoErr := td.getOrBuildTypeInfo(r)
+	if typeInfoErr != nil {
+		err.SetError(typeInfoErr)
+		return nil
+	}
+	r.defIdToTypeDef[id] = td
+	r.recordRemoteTypeDef(typeKey)
 	context.readTypeInfos = append(context.readTypeInfos, typeInfo)
 	return typeInfo
 }
 
+func (r *TypeResolver) localTypeInfoForTypeDef(td *TypeDef) *TypeInfo {
+	var typeInfo *TypeInfo
+	if td.registerByName {
+		if td.nsName == nil || td.typeName == nil {
+			return nil
+		}
+		typeInfo = r.nsTypeToTypeInfo[nsTypeKey{td.nsName.Hashcode, td.typeName.Hashcode}]
+		if typeInfo == nil {
+			namespace, nsErr := r.namespaceDecoder.Decode(td.nsName.Data, td.nsName.Encoding)
+			typeName, nameErr := r.typeNameDecoder.Decode(td.typeName.Data, td.typeName.Encoding)
+			if nsErr == nil && nameErr == nil {
+				typeInfo = r.namedTypeToTypeInfo[[2]string{namespace, typeName}]
+			}
+		}
+	} else if td.userTypeId != invalidUserTypeID {
+		typeInfo = r.userTypeIdToTypeInfo[td.userTypeId]
+	}
+	if typeInfo == nil {
+		return nil
+	}
+	if typeInfo.Type != nil && typeInfo.Type.Kind() == reflect.Ptr {
+		if valueInfo := r.typesInfo[typeInfo.Type.Elem()]; valueInfo != nil {
+			typeInfo = valueInfo
+		}
+	}
+	return typeInfo
+}
+
+func (r *TypeResolver) localTypeDef(typeInfo *TypeInfo) (*TypeDef, error) {
+	if typeInfo == nil || typeInfo.Type == nil {
+		return nil, nil
+	}
+	type_ := typeInfo.Type
+	if type_.Kind() == reflect.Ptr {
+		type_ = type_.Elem()
+	}
+	return r.getTypeDef(type_, true)
+}
+
 func (r *TypeResolver) createSerializer(type_ reflect.Type, mapInStruct bool) (s Serializer, err error) {
 	if info, ok := getOptionalInfo(type_); ok {
 		optionalType := type_
@@ -2081,9 +2181,7 @@ func (r *TypeResolver) resolveTypeInfoByMetaBytes(nsBytes, typeBytes *MetaString
 
 	nameKey := [2]string{ns, typeName}
 	if typeInfo, exists := r.namedTypeToTypeInfo[nameKey]; exists {
-		if len(r.nsTypeToTypeInfo) < maxCachedNamedTypeInfos {
-			r.nsTypeToTypeInfo[compositeKey] = typeInfo
-		}
+		r.nsTypeToTypeInfo[compositeKey] = typeInfo
 		return typeInfo
 	}
 
diff --git a/java/fory-core/src/main/java/org/apache/fory/Fory.java b/java/fory-core/src/main/java/org/apache/fory/Fory.java
index b3629cee9..5ad083d69 100644
--- a/java/fory-core/src/main/java/org/apache/fory/Fory.java
+++ b/java/fory-core/src/main/java/org/apache/fory/Fory.java
@@ -123,9 +123,12 @@ public final class Fory implements BaseFory {
   public Fory(ForyBuilder builder, ClassLoader classLoader, SharedRegistry sharedRegistry) {
     // Prefer the explicit constructor argument over retaining loader state on the builder used to
     // create thread-safe factories.
+    config = new Config(builder);
     if (sharedRegistry == null) {
       sharedRegistry = new SharedRegistry();
     }
+    sharedRegistry.setRemoteSchemaLimits(
+        config.maxSchemaVersionsPerType(), config.maxAverageSchemaVersionsPerType());
     if (classLoader == null) {
       classLoader = Thread.currentThread().getContextClassLoader();
       if (classLoader == null) {
@@ -134,7 +137,6 @@ public final class Fory implements BaseFory {
     }
     this.sharedRegistry = sharedRegistry;
     this.classLoader = classLoader;
-    config = new Config(builder);
     headerBitmap = config.isXlang() ? isCrossLanguageFlag : 0;
     RefWriter refWriter;
     RefReader refReader;
diff --git a/java/fory-core/src/main/java/org/apache/fory/config/Config.java b/java/fory-core/src/main/java/org/apache/fory/config/Config.java
index 8c9f18d82..2b8db3ec6 100644
--- a/java/fory-core/src/main/java/org/apache/fory/config/Config.java
+++ b/java/fory-core/src/main/java/org/apache/fory/config/Config.java
@@ -64,6 +64,10 @@ public class Config implements Serializable {
   private final boolean serializeEnumByName;
   private final int bufferSizeLimitBytes;
   private final int maxDepth;
+  private final int maxTypeFields;
+  private final int maxTypeMetaBytes;
+  private final int maxSchemaVersionsPerType;
+  private final int maxAverageSchemaVersionsPerType;
   private final float mapRefLoadFactor;
   private final boolean forVirtualThread;
 
@@ -106,6 +110,10 @@ public class Config implements Serializable {
     serializeEnumByName = builder.serializeEnumByName;
     bufferSizeLimitBytes = builder.bufferSizeLimitBytes;
     maxDepth = builder.maxDepth;
+    maxTypeFields = builder.maxTypeFields;
+    maxTypeMetaBytes = builder.maxTypeMetaBytes;
+    maxSchemaVersionsPerType = builder.maxSchemaVersionsPerType;
+    maxAverageSchemaVersionsPerType = builder.maxAverageSchemaVersionsPerType;
     mapRefLoadFactor = builder.mapRefLoadFactor;
     forVirtualThread = builder.forVirtualThread;
   }
@@ -292,6 +300,26 @@ public class Config implements Serializable {
     return maxDepth;
   }
 
+  /** Returns the maximum number of fields accepted in one received struct TypeDef. */
+  public int maxTypeFields() {
+    return maxTypeFields;
+  }
+
+  /** Returns the maximum body size accepted for one received TypeDef. */
+  public int maxTypeMetaBytes() {
+    return maxTypeMetaBytes;
+  }
+
+  /** Returns the maximum accepted remote metadata versions for one logical type. */
+  public int maxSchemaVersionsPerType() {
+    return maxSchemaVersionsPerType;
+  }
+
+  /** Returns the maximum average accepted remote metadata versions across logical types. */
+  public int maxAverageSchemaVersionsPerType() {
+    return maxAverageSchemaVersionsPerType;
+  }
+
   /** Returns loadFactor of MacRef's writtenObjects. */
   public float mapRefLoadFactor() {
     return mapRefLoadFactor;
@@ -336,6 +364,10 @@ public class Config implements Serializable {
         && deserializeUnknownClass == config.deserializeUnknownClass
         && xlang == config.xlang
         && compatible == config.compatible
+        && maxTypeFields == config.maxTypeFields
+        && maxTypeMetaBytes == config.maxTypeMetaBytes
+        && maxSchemaVersionsPerType == config.maxSchemaVersionsPerType
+        && maxAverageSchemaVersionsPerType == config.maxAverageSchemaVersionsPerType
         && Objects.equals(defaultJDKStreamSerializerType, config.defaultJDKStreamSerializerType)
         && longEncoding == config.longEncoding
         && forVirtualThread == config.forVirtualThread;
@@ -367,6 +399,10 @@ public class Config implements Serializable {
         requireClassRegistration,
         suppressClassRegistrationWarnings,
         registerGuavaTypes,
+        maxTypeFields,
+        maxTypeMetaBytes,
+        maxSchemaVersionsPerType,
+        maxAverageSchemaVersionsPerType,
         metaShareEnabled,
         scopedMetaShareEnabled,
         metaCompressor,
diff --git a/java/fory-core/src/main/java/org/apache/fory/config/ForyBuilder.java b/java/fory-core/src/main/java/org/apache/fory/config/ForyBuilder.java
index 93bd88aab..8625c85c3 100644
--- a/java/fory-core/src/main/java/org/apache/fory/config/ForyBuilder.java
+++ b/java/fory-core/src/main/java/org/apache/fory/config/ForyBuilder.java
@@ -99,6 +99,10 @@ public final class ForyBuilder {
   Integer bufferSizeLimitBytes = -1;
   MetaCompressor metaCompressor = new DeflaterMetaCompressor();
   int maxDepth = 50;
+  int maxTypeFields = 512;
+  int maxTypeMetaBytes = 4096;
+  int maxSchemaVersionsPerType = 10;
+  int maxAverageSchemaVersionsPerType = 3;
   float mapRefLoadFactor = 0.51f;
   boolean forVirtualThread = false;
   TypeChecker typeChecker;
@@ -511,6 +515,62 @@ public final class ForyBuilder {
     return this;
   }
 
+  /**
+   * Sets the maximum number of fields accepted in one received struct TypeDef.
+   *
+   * <p>This limit applies only to cold remote metadata parse paths.
+   */
+  public ForyBuilder withMaxTypeFields(int maxTypeFields) {
+    Preconditions.checkArgument(
+        maxTypeFields > 0, "maxTypeFields must be positive but got %s", maxTypeFields);
+    this.maxTypeFields = maxTypeFields;
+    recordAction(b -> b.withMaxTypeFields(maxTypeFields));
+    return this;
+  }
+
+  /**
+   * Sets the maximum body size accepted for one received TypeDef.
+   *
+   * <p>This limit excludes the 8-byte metadata header and any extended-size varint bytes.
+   */
+  public ForyBuilder withMaxTypeMetaBytes(int maxTypeMetaBytes) {
+    Preconditions.checkArgument(
+        maxTypeMetaBytes > 0, "maxTypeMetaBytes must be positive but got %s", maxTypeMetaBytes);
+    this.maxTypeMetaBytes = maxTypeMetaBytes;
+    recordAction(b -> b.withMaxTypeMetaBytes(maxTypeMetaBytes));
+    return this;
+  }
+
+  /**
+   * Sets the maximum number of accepted remote metadata versions for one logical type.
+   *
+   * <p>This limit applies only to cold remote metadata miss paths.
+   */
+  public ForyBuilder withMaxSchemaVersionsPerType(int maxSchemaVersionsPerType) {
+    Preconditions.checkArgument(
+        maxSchemaVersionsPerType > 0,
+        "maxSchemaVersionsPerType must be positive but got %s",
+        maxSchemaVersionsPerType);
+    this.maxSchemaVersionsPerType = maxSchemaVersionsPerType;
+    recordAction(b -> b.withMaxSchemaVersionsPerType(maxSchemaVersionsPerType));
+    return this;
+  }
+
+  /**
+   * Sets the maximum average number of accepted remote metadata versions across logical types.
+   *
+   * <p>The global limit has an internal floor so small type universes can still evolve normally.
+   */
+  public ForyBuilder withMaxAverageSchemaVersionsPerType(int maxAverageSchemaVersionsPerType) {
+    Preconditions.checkArgument(
+        maxAverageSchemaVersionsPerType > 0,
+        "maxAverageSchemaVersionsPerType must be positive but got %s",
+        maxAverageSchemaVersionsPerType);
+    this.maxAverageSchemaVersionsPerType = maxAverageSchemaVersionsPerType;
+    recordAction(b -> b.withMaxAverageSchemaVersionsPerType(maxAverageSchemaVersionsPerType));
+    return this;
+  }
+
   /** Set loadFactor of MapRefResolver writtenObjects. Default value is 0.51 */
   public ForyBuilder withMapRefLoadFactor(float loadFactor) {
     Preconditions.checkArgument(
diff --git a/java/fory-core/src/main/java/org/apache/fory/meta/NativeTypeDefDecoder.java b/java/fory-core/src/main/java/org/apache/fory/meta/NativeTypeDefDecoder.java
index 7115414ab..726603b36 100644
--- a/java/fory-core/src/main/java/org/apache/fory/meta/NativeTypeDefDecoder.java
+++ b/java/fory-core/src/main/java/org/apache/fory/meta/NativeTypeDefDecoder.java
@@ -46,8 +46,6 @@ import org.apache.fory.util.Preconditions;
  * href="https://fory.apache.org/docs/specification/fory_java_serialization_spec">...</a>
  */
 class NativeTypeDefDecoder {
-  private static final int MAX_TYPE_DEF_SIZE_BYTES = 16 * 1024 * 1024;
-
   static Tuple2<byte[], byte[]> decodeTypeDefBuf(
       MemoryBuffer inputBuffer, TypeResolver resolver, long id) {
     if ((id & TypeDef.RESERVED_META_FLAGS) != 0) {
@@ -58,12 +56,16 @@ class NativeTypeDefDecoder {
     int size = (int) (id & META_SIZE_MASKS);
     if (size == META_SIZE_MASKS) {
       int moreSize = inputBuffer.readVarUInt32Small14();
+      if (moreSize < 0 || moreSize > Integer.MAX_VALUE - size) {
+        throw new DeserializationException("Invalid TypeDef metadata size " + moreSize);
+      }
       encoded.writeVarUInt32(moreSize);
       size += moreSize;
     }
-    if (size > MAX_TYPE_DEF_SIZE_BYTES) {
-      throw new DeserializationException("TypeDef metadata size exceeds the maximum size");
-    }
+    checkTypeMetaBodySize(size, resolver.getConfig().maxTypeMetaBytes());
+    // Keep this as the first body materialization: readBytes checks readable
+    // bytes before allocating/copying. The size limit above is not proof that
+    // the declared bytes exist in the input.
     byte[] encodedTypeDef = inputBuffer.readBytes(size);
     encoded.writeBytes(encodedTypeDef);
     if ((id & COMPRESS_META_FLAG) != 0) {
@@ -71,11 +73,35 @@ class NativeTypeDefDecoder {
           resolver
               .getConfig()
               .getMetaCompressor()
-              .decompress(encodedTypeDef, 0, size, MAX_TYPE_DEF_SIZE_BYTES);
+              .decompress(encodedTypeDef, 0, size, resolver.getConfig().maxTypeMetaBytes());
     }
     return Tuple2.of(encodedTypeDef, encoded.getBytes(0, encoded.writerIndex()));
   }
 
+  static void checkTypeMetaBodySize(int size, int maxTypeMetaBytes) {
+    if (size > maxTypeMetaBytes) {
+      throw new DeserializationException(
+          "Type metadata body size "
+              + size
+              + " exceeds maxTypeMetaBytes "
+              + maxTypeMetaBytes
+              + ". The data may be malicious. If the data is not malicious, please increase "
+              + "maxTypeMetaBytes.");
+    }
+  }
+
+  static void checkTypeMetaFieldCount(long fieldCount, int maxTypeFields) {
+    if (fieldCount > maxTypeFields) {
+      throw new DeserializationException(
+          "Type metadata field count "
+              + fieldCount
+              + " exceeds maxTypeFields "
+              + maxTypeFields
+              + ". The data may be malicious. If the data is not malicious, please increase "
+              + "maxTypeFields.");
+    }
+  }
+
   public static TypeDef decodeTypeDef(ClassResolver resolver, MemoryBuffer buffer, long id) {
     Tuple2<byte[], byte[]> decoded = decodeTypeDefBuf(buffer, resolver, id);
     MemoryBuffer typeDefBuf = MemoryBuffer.fromByteArray(decoded.f0);
@@ -95,6 +121,8 @@ class NativeTypeDefDecoder {
     ClassSpec classSpec = null;
     Class<?> rootClass = null;
     boolean rootClassLayerRegistered = false;
+    int maxTypeFields = resolver.getConfig().maxTypeFields();
+    long totalFields = 0;
     for (int i = 0; i < numClasses; i++) {
       // | num fields + register flag | header + package name | header + class name
       // | header + type id + field name | next field info | ... |
@@ -104,6 +132,8 @@ class NativeTypeDefDecoder {
       }
       boolean isRegistered = (currentClassHeader & 0b1) != 0;
       int numFields = currentClassHeader >>> 1;
+      totalFields += numFields;
+      checkTypeMetaFieldCount(totalFields, maxTypeFields);
       Class<?> currentClass = null;
       if (isRegistered) {
         int typeId = typeDefBuf.readUInt8();
@@ -136,6 +166,8 @@ class NativeTypeDefDecoder {
           classSpec = new ClassSpec(cls, typeId, resolver.getUserTypeIdForTypeDef(cls));
           currentClass = cls;
         } else {
+          // `loadClassForMeta` keeps name-level checks before Class.forName; do not replace this
+          // metadata path with direct class loading from the remote TypeDef name.
           Class<?> cls =
               resolver.loadClassForMeta(
                   decodedSpec.entireClassName, decodedSpec.isEnum, decodedSpec.dimension);
@@ -175,9 +207,8 @@ class NativeTypeDefDecoder {
     }
     Preconditions.checkNotNull(classSpec);
     boolean hasFieldMetadata = !classFields.isEmpty();
-    if (!Types.isStructType(rootTypeId) && hasFieldMetadata) {
-      throw new DeserializationException("Non-struct TypeDef cannot carry field metadata");
-    }
+    // Native TypeDef can carry class-layer fields even when the root wire type is an enum,
+    // map, or other non-struct wrapper. Validate the resolved root class kind instead.
     if (rootClass != null) {
       int expectedRootTypeId = resolver.getTypeDefRootTypeId(rootClass, hasFieldMetadata);
       if (!isCompatibleRootKind(expectedRootTypeId, rootTypeId, !rootClassLayerRegistered)) {
diff --git a/java/fory-core/src/main/java/org/apache/fory/meta/TypeDef.java b/java/fory-core/src/main/java/org/apache/fory/meta/TypeDef.java
index 26865cb32..5e8b33eac 100644
--- a/java/fory-core/src/main/java/org/apache/fory/meta/TypeDef.java
+++ b/java/fory-core/src/main/java/org/apache/fory/meta/TypeDef.java
@@ -89,7 +89,8 @@ public class TypeDef implements Serializable {
   public static void skipTypeDef(MemoryBuffer buffer, long id) {
     // Header-cache hits intentionally treat the current body as opaque bytes and skip by the size
     // in the current header. Parsed TypeDefs are published to the cache only after successful body
-    // parse and 52-bit metadata-hash validation; cache hits must not reparse or rehash that body.
+    // parse and 52-bit metadata-hash validation; cache hits must not reparse, rehash, allocate, or
+    // otherwise materialize that body.
     int size = (int) (id & META_SIZE_MASKS);
     if (size == META_SIZE_MASKS) {
       int extendedSize = buffer.readVarUInt32Small14();
@@ -321,6 +322,11 @@ public class TypeDef implements Serializable {
     return NativeTypeDefDecoder.decodeTypeDef((ClassResolver) resolver, buffer, buffer.readInt64());
   }
 
+  /** Decode an encoded class definition. */
+  public static TypeDef readTypeDef(TypeResolver resolver, byte[] encoded) {
+    return readTypeDef(resolver, MemoryBuffer.fromByteArray(encoded));
+  }
+
   /** Read class definition from buffer. */
   public static TypeDef readTypeDef(TypeResolver resolver, MemoryBuffer buffer, long header) {
     if (resolver.isCrossLanguage()) {
@@ -329,6 +335,17 @@ public class TypeDef implements Serializable {
     return NativeTypeDefDecoder.decodeTypeDef((ClassResolver) resolver, buffer, header);
   }
 
+  /** Read encoded class definition bytes from buffer. */
+  public static byte[] readTypeDefBytes(TypeResolver resolver, MemoryBuffer buffer, long header) {
+    if (resolver.isCrossLanguage()) {
+      if ((header & COMPRESS_META_FLAG) != 0) {
+        throw new DeserializationException("Compressed xlang TypeDef is not supported");
+      }
+      return NativeTypeDefDecoder.decodeTypeDefBuf(buffer, resolver, header).f1;
+    }
+    return NativeTypeDefDecoder.decodeTypeDefBuf(buffer, resolver, header).f1;
+  }
+
   /**
    * Consolidate fields of <code>typeDef</code> with <code>cls</code>. If some field exists in
    * <code>cls</code> but not in <code>typeDef</code>, it won't be returned in final collection. If
diff --git a/java/fory-core/src/main/java/org/apache/fory/meta/TypeDefDecoder.java b/java/fory-core/src/main/java/org/apache/fory/meta/TypeDefDecoder.java
index 83ca2287b..587e36919 100644
--- a/java/fory-core/src/main/java/org/apache/fory/meta/TypeDefDecoder.java
+++ b/java/fory-core/src/main/java/org/apache/fory/meta/TypeDefDecoder.java
@@ -20,6 +20,7 @@
 package org.apache.fory.meta;
 
 import static org.apache.fory.meta.Encoders.fieldNameEncodings;
+import static org.apache.fory.meta.NativeTypeDefDecoder.checkTypeMetaFieldCount;
 import static org.apache.fory.meta.NativeTypeDefDecoder.decodeTypeDefBuf;
 import static org.apache.fory.meta.NativeTypeDefDecoder.readPkgName;
 import static org.apache.fory.meta.NativeTypeDefDecoder.readTypeName;
@@ -82,6 +83,7 @@ class TypeDefDecoder {
         }
         numFields += extraFields;
       }
+      checkTypeMetaFieldCount(numFields, resolver.getConfig().maxTypeFields());
       if (named) {
         String namespace = readPkgName(buffer);
         String typeName = readTypeName(buffer);
@@ -90,7 +92,7 @@ class TypeDefDecoder {
         }
         TypeInfo userTypeInfo = resolver.getUserTypeInfo(namespace, typeName);
         if (userTypeInfo == null) {
-          classSpec = new ClassSpec(UnknownClass.UnknownStruct.class, typeId, -1);
+          classSpec = unknownNamedClassSpec(namespace, typeName, typeId);
         } else {
           validateRegisteredTypeDefKind(userTypeInfo, typeId);
           classSpec = new ClassSpec(userTypeInfo.getType(), typeId, userTypeInfo.getUserTypeId());
@@ -116,7 +118,7 @@ class TypeDefDecoder {
         String typeName = readTypeName(buffer);
         TypeInfo userTypeInfo = resolver.getUserTypeInfo(namespace, typeName);
         if (userTypeInfo == null) {
-          classSpec = new ClassSpec(UnknownClass.UnknownStruct.class, typeId, -1);
+          classSpec = unknownNamedClassSpec(namespace, typeName, typeId);
         } else {
           validateRegisteredTypeDefKind(userTypeInfo, typeId);
           classSpec = new ClassSpec(userTypeInfo.getType(), typeId, userTypeInfo.getUserTypeId());
@@ -169,6 +171,13 @@ class TypeDefDecoder {
     }
   }
 
+  private static ClassSpec unknownNamedClassSpec(String namespace, String typeName, int typeId) {
+    String className = namespace.isEmpty() ? typeName : namespace + "." + typeName;
+    ClassSpec classSpec = new ClassSpec(className, Types.isEnumType(typeId), false, 0, typeId, -1);
+    classSpec.type = UnknownClass.UnknownStruct.class;
+    return classSpec;
+  }
+
   private static boolean isStructCompatibilityVariant(int registeredTypeId, int typeId) {
     boolean registeredIdStruct =
         registeredTypeId == Types.STRUCT || registeredTypeId == Types.COMPATIBLE_STRUCT;
diff --git a/java/fory-core/src/main/java/org/apache/fory/resolver/ClassResolver.java b/java/fory-core/src/main/java/org/apache/fory/resolver/ClassResolver.java
index fe5852012..4104d023f 100644
--- a/java/fory-core/src/main/java/org/apache/fory/resolver/ClassResolver.java
+++ b/java/fory-core/src/main/java/org/apache/fory/resolver/ClassResolver.java
@@ -2095,7 +2095,7 @@ public class ClassResolver extends TypeResolver {
    *
    * <p>For named-class tokens, this method enforces deserialization class policy before returning
    * the class, including the disallowed list and registration or TypeChecker checks. For registered
-   * type-id tokens, it returns the registered class whose admission was already checked during
+   * type-id tokens, it returns the registered class whose class policy was already checked during
    * registration.
    *
    * <p>Note that the object of the class can be non-serializable. For serializable object, {@link
diff --git a/java/fory-core/src/main/java/org/apache/fory/resolver/SharedRegistry.java b/java/fory-core/src/main/java/org/apache/fory/resolver/SharedRegistry.java
index a3f573afd..fa72d2f1f 100644
--- a/java/fory-core/src/main/java/org/apache/fory/resolver/SharedRegistry.java
+++ b/java/fory-core/src/main/java/org/apache/fory/resolver/SharedRegistry.java
@@ -22,17 +22,18 @@ package org.apache.fory.resolver;
 import java.lang.reflect.Member;
 import java.util.ArrayList;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.IdentityHashMap;
 import java.util.List;
 import java.util.Objects;
 import java.util.SortedMap;
 import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.atomic.AtomicInteger;
 import org.apache.fory.annotation.Internal;
 import org.apache.fory.codegen.CodeGenerator;
 import org.apache.fory.collection.BiMap;
 import org.apache.fory.collection.ConcurrentIdentityMap;
 import org.apache.fory.collection.Tuple2;
+import org.apache.fory.exception.ForyException;
 import org.apache.fory.meta.EncodedMetaString;
 import org.apache.fory.meta.Encoders;
 import org.apache.fory.meta.MetaString;
@@ -57,12 +58,13 @@ import org.apache.fory.type.DescriptorGrouper;
 public final class SharedRegistry {
   private static final int MAX_CACHED_ENCODED_META_STRINGS = 32768;
   private static final int MAX_CACHED_ENCODED_META_STRING_LENGTH = 2048;
-  private static final int MAX_CACHED_TYPE_DEFS = 8192;
+  private static final int MIN_REMOTE_TYPE_DEF_LIMIT = 8192;
 
   final ConcurrentIdentityMap<Class<?>, TypeDef> typeDefMap = new ConcurrentIdentityMap<>();
   final ConcurrentIdentityMap<Class<?>, TypeDef> currentLayerTypeDef =
       new ConcurrentIdentityMap<>();
   final ConcurrentHashMap<Long, TypeDef> typeDefById = new ConcurrentHashMap<>();
+  final ConcurrentHashMap<Long, TypeDef> remoteTypeDefById = new ConcurrentHashMap<>();
   final ConcurrentHashMap<Tuple2<Class<?>, Boolean>, SortedMap<Member, Descriptor>>
       descriptorsCache = new ConcurrentHashMap<>();
   final ConcurrentHashMap<FieldDescriptorsKey, List<Descriptor>> fieldDescriptorsCache =
@@ -90,10 +92,39 @@ public final class SharedRegistry {
   final StaticGeneratedSerializerRegistry staticGeneratedSerializerRegistry =
       new StaticGeneratedSerializerRegistry();
   private final Object metaStringCacheLock = new Object();
-  private final AtomicInteger cachedTypeDefCount = new AtomicInteger();
+  private volatile int maxSchemaVersionsPerType = -1;
+  private volatile int maxAverageSchemaVersionsPerType = -1;
+  private final HashMap<Object, Integer> remoteTypeDefVersionsByType = new HashMap<>();
+  private int totalAcceptedSchemaVersions;
   volatile IdentityHashMap<Class<?>, Integer> registeredClassIdMap;
   volatile BiMap<String, Class<?>> registeredClasses;
 
+  public SharedRegistry() {}
+
+  public synchronized void setRemoteSchemaLimits(
+      int maxSchemaVersionsPerType, int maxAverageSchemaVersionsPerType) {
+    if (maxSchemaVersionsPerType <= 0) {
+      throw new IllegalArgumentException(
+          "maxSchemaVersionsPerType must be positive but got " + maxSchemaVersionsPerType);
+    }
+    if (maxAverageSchemaVersionsPerType <= 0) {
+      throw new IllegalArgumentException(
+          "maxAverageSchemaVersionsPerType must be positive but got "
+              + maxAverageSchemaVersionsPerType);
+    }
+    int currentMaxSchemaVersionsPerType = this.maxSchemaVersionsPerType;
+    if (currentMaxSchemaVersionsPerType < 0) {
+      this.maxAverageSchemaVersionsPerType = maxAverageSchemaVersionsPerType;
+      this.maxSchemaVersionsPerType = maxSchemaVersionsPerType;
+      return;
+    }
+    if (currentMaxSchemaVersionsPerType != maxSchemaVersionsPerType
+        || this.maxAverageSchemaVersionsPerType != maxAverageSchemaVersionsPerType) {
+      throw new IllegalArgumentException(
+          "SharedRegistry cannot be reused with different remote schema limits");
+    }
+  }
+
   synchronized void setRegistrationIfAbsent(
       IdentityHashMap<Class<?>, Integer> candidateRegisteredClassIdMap,
       BiMap<String, Class<?>> candidateRegisteredClasses) {
@@ -157,35 +188,88 @@ public final class SharedRegistry {
     if (existing != null) {
       return existing;
     }
-    if (!reserveTypeDefCacheSlot()) {
-      return typeDef;
-    }
     existing = typeDefById.putIfAbsent(id, typeDef);
+    return existing == null ? typeDef : existing;
+  }
+
+  synchronized TypeDef getOrCreateRemoteTypeDef(TypeDef typeDef, Object remoteTypeKey) {
+    long id = typeDef.getId();
+    TypeDef existing = remoteTypeDefById.get(id);
+    if (existing != null) {
+      return existing;
+    }
+    int versionsForType = checkRemoteTypeLimit(remoteTypeKey);
+    TypeDef canonicalTypeDef = typeDefById.putIfAbsent(id, typeDef);
+    if (canonicalTypeDef == null) {
+      canonicalTypeDef = typeDef;
+    }
+    existing = remoteTypeDefById.putIfAbsent(id, canonicalTypeDef);
     if (existing != null) {
-      cachedTypeDefCount.decrementAndGet();
       return existing;
     }
-    return typeDef;
+    remoteTypeDefVersionsByType.put(remoteTypeKey, versionsForType + 1);
+    totalAcceptedSchemaVersions++;
+    return canonicalTypeDef;
   }
 
-  private boolean reserveTypeDefCacheSlot() {
-    while (true) {
-      int count = cachedTypeDefCount.get();
-      int mapSize = typeDefById.size();
-      if (mapSize > count) {
-        if (cachedTypeDefCount.compareAndSet(count, mapSize)) {
-          count = mapSize;
-        } else {
-          continue;
-        }
-      }
-      if (count >= MAX_CACHED_TYPE_DEFS) {
-        return false;
-      }
-      if (cachedTypeDefCount.compareAndSet(count, count + 1)) {
-        return true;
-      }
+  synchronized void checkRemoteTypeDefLimit(TypeDef typeDef, Object remoteTypeKey) {
+    if (remoteTypeDefById.containsKey(typeDef.getId())) {
+      return;
+    }
+    checkRemoteTypeLimit(remoteTypeKey);
+  }
+
+  private int checkRemoteTypeLimit(Object remoteTypeKey) {
+    int versionsForType = remoteTypeDefVersionsByType.getOrDefault(remoteTypeKey, 0);
+    int maxSchemaVersionsPerType = maxSchemaVersionsPerType();
+    if (versionsForType >= maxSchemaVersionsPerType) {
+      throw new ForyException(
+          "Remote schema version limit exceeded for type "
+              + remoteTypeKey
+              + ": "
+              + versionsForType
+              + " >= "
+              + maxSchemaVersionsPerType
+              + ". The data may be malicious. If the data is not malicious, please increase "
+              + "maxSchemaVersionsPerType.");
+    }
+    int acceptedRemoteTypeCount =
+        versionsForType == 0
+            ? remoteTypeDefVersionsByType.size() + 1
+            : remoteTypeDefVersionsByType.size();
+    int maxAverageSchemaVersionsPerType = maxAverageSchemaVersionsPerType();
+    long globalLimit =
+        Math.max(
+            (long) MIN_REMOTE_TYPE_DEF_LIMIT,
+            (long) acceptedRemoteTypeCount * maxAverageSchemaVersionsPerType);
+    if (totalAcceptedSchemaVersions >= globalLimit) {
+      throw new ForyException(
+          "Remote schema version limit exceeded: "
+              + totalAcceptedSchemaVersions
+              + " metadata versions for "
+              + acceptedRemoteTypeCount
+              + " accepted remote types exceeds the average limit "
+              + maxAverageSchemaVersionsPerType
+              + ". The data may be malicious. If the data is not malicious, please increase "
+              + "maxAverageSchemaVersionsPerType.");
+    }
+    return versionsForType;
+  }
+
+  private int maxSchemaVersionsPerType() {
+    int maxSchemaVersionsPerType = this.maxSchemaVersionsPerType;
+    if (maxSchemaVersionsPerType < 0) {
+      throw new IllegalStateException("SharedRegistry remote schema limits are not initialized");
+    }
+    return maxSchemaVersionsPerType;
+  }
+
+  private int maxAverageSchemaVersionsPerType() {
+    int maxSchemaVersionsPerType = this.maxSchemaVersionsPerType;
+    if (maxSchemaVersionsPerType < 0) {
+      throw new IllegalStateException("SharedRegistry remote schema limits are not initialized");
     }
+    return maxAverageSchemaVersionsPerType;
   }
 
   EncodedMetaString getPackageEncodedMetaString(String string) {
diff --git a/java/fory-core/src/main/java/org/apache/fory/resolver/TypeResolver.java b/java/fory-core/src/main/java/org/apache/fory/resolver/TypeResolver.java
index 80c9f5998..87a18426e 100644
--- a/java/fory-core/src/main/java/org/apache/fory/resolver/TypeResolver.java
+++ b/java/fory-core/src/main/java/org/apache/fory/resolver/TypeResolver.java
@@ -123,7 +123,6 @@ public abstract class TypeResolver {
   static final int INTERNAL_NATIVE_ID_LIMIT = 250;
   private static final GenericType OBJECT_GENERIC_TYPE = GenericType.build(Object.class);
   private static final float TYPE_ID_MAP_LOAD_FACTOR = 0.5f;
-  private static final int MAX_CACHED_TYPE_DEFS = 8192;
   static final long MAX_USER_TYPE_ID = 0xffff_fffEL;
 
   private static final class TransformedTypeInfo {
@@ -631,7 +630,7 @@ public abstract class TypeResolver {
         break;
       case Types.COMPATIBLE_STRUCT:
       case Types.NAMED_COMPATIBLE_STRUCT:
-        typeInfo = readSharedClassMeta(readContext);
+        typeInfo = readSharedClassTypeInfo(readContext, null);
         break;
       case Types.NAMED_ENUM:
       case Types.NAMED_STRUCT:
@@ -640,7 +639,7 @@ public abstract class TypeResolver {
         if (!metaContextShareEnabled) {
           typeInfo = readTypeInfoFromBytes(readContext, typeInfoCache, typeId);
         } else {
-          typeInfo = readSharedClassMeta(readContext);
+          typeInfo = readSharedClassTypeInfo(readContext, null);
         }
         break;
       case Types.LIST:
@@ -726,7 +725,7 @@ public abstract class TypeResolver {
         break;
       case Types.COMPATIBLE_STRUCT:
       case Types.NAMED_COMPATIBLE_STRUCT:
-        typeInfo = readSharedClassMeta(readContext);
+        typeInfo = readSharedClassTypeInfo(readContext, null);
         break;
       case Types.NAMED_ENUM:
       case Types.NAMED_STRUCT:
@@ -735,7 +734,7 @@ public abstract class TypeResolver {
         if (!metaContextShareEnabled) {
           typeInfo = readTypeInfoFromBytes(readContext, typeInfoCache, typeId);
         } else {
-          typeInfo = readSharedClassMeta(readContext);
+          typeInfo = readSharedClassTypeInfo(readContext, null);
         }
         break;
       case Types.LIST:
@@ -775,7 +774,7 @@ public abstract class TypeResolver {
         break;
       case Types.COMPATIBLE_STRUCT:
       case Types.NAMED_COMPATIBLE_STRUCT:
-        typeInfo = readSharedClassMeta(readContext);
+        typeInfo = readSharedClassTypeInfo(readContext, null);
         break;
       case Types.NAMED_ENUM:
       case Types.NAMED_STRUCT:
@@ -785,7 +784,7 @@ public abstract class TypeResolver {
           typeInfo = readTypeInfoFromBytes(readContext, classInfoHolder.typeInfo, typeId);
           updateCache = true;
         } else {
-          typeInfo = readSharedClassMeta(readContext);
+          typeInfo = readSharedClassTypeInfo(readContext, null);
         }
         break;
       case Types.LIST:
@@ -849,11 +848,16 @@ public abstract class TypeResolver {
     return loadBytesToTypeInfo(header, namespaceBytes, simpleClassNameBytes);
   }
 
-  /**
-   * Reads shared class metadata from buffer. This is the shared implementation used by both
-   * ClassResolver and XtypeResolver.
-   */
-  protected final TypeInfo readSharedClassMeta(ReadContext readContext) {
+  public final TypeInfo readSharedClassMeta(ReadContext readContext, Class<?> targetClass) {
+    TypeInfo typeInfo = readSharedClassTypeInfo(readContext, targetClass);
+    Class<?> readClass = typeInfo.getType();
+    if (targetClass != readClass) {
+      return getTargetTypeInfo(typeInfo, targetClass);
+    }
+    return typeInfo;
+  }
+
+  private TypeInfo readSharedClassTypeInfo(ReadContext readContext, Class<?> targetClass) {
     MemoryBuffer buffer = readContext.getBuffer();
     MetaReadContext metaReadContext = readContext.getMetaReadContext();
     assert metaReadContext != null : SET_META_READ_CONTEXT_MSG;
@@ -867,19 +871,15 @@ public abstract class TypeResolver {
     } else {
       // New type in stream, with optimized reuse by validated TypeDef header. A header-cache
       // hit intentionally skips the body without rehashing: entries are published only after the
-      // TypeDef body has parsed successfully and matched the 52-bit metadata hash.
+      // TypeDef body has parsed successfully and matched the 52-bit metadata hash. Do not add
+      // body/hash/schema-limit/exact-local checks here; the header-miss path owns them before
+      // cache publish.
       long id = buffer.readInt64();
       typeInfo = extRegistry.typeInfoByTypeDefId.get(id);
       if (typeInfo != null) {
         TypeDef.skipTypeDef(buffer, id);
       } else {
-        TypeDef typeDef = sharedRegistry.typeDefById.get(id);
-        if (typeDef != null) {
-          TypeDef.skipTypeDef(buffer, id);
-        } else {
-          typeDef = readTypeDef(buffer, id);
-        }
-        typeInfo = buildMetaSharedTypeInfo(typeDef);
+        typeInfo = readSharedTypeDefInfo(buffer, id, targetClass);
       }
       // index == readTypeInfos.size() since types are written sequentially
       metaReadContext.readTypeInfos.add(typeInfo);
@@ -887,14 +887,20 @@ public abstract class TypeResolver {
     return typeInfo;
   }
 
-  public final TypeInfo readSharedClassMeta(ReadContext readContext, Class<?> targetClass) {
-    TypeInfo typeInfo = readSharedClassMeta(readContext);
-    Class<?> readClass = typeInfo.getType();
-    // replace target class if needed
-    if (targetClass != readClass) {
-      return getTargetTypeInfo(typeInfo, targetClass);
+  private TypeInfo readSharedTypeDefInfo(MemoryBuffer buffer, long id, Class<?> targetClass) {
+    TypeDef typeDef = sharedRegistry.remoteTypeDefById.get(id);
+    if (typeDef != null) {
+      TypeDef.skipTypeDef(buffer, id);
+      return buildMetaSharedTypeInfo(typeDef);
     }
-    return typeInfo;
+    typeDef = TypeDef.readTypeDef(this, buffer, id);
+    // The target check is needed only for a newly parsed TypeDef, before it can be
+    // cached or counted. Cache hits were already accepted; the caller applies target
+    // adaptation after resolving TypeInfo.
+    if (targetClass != null) {
+      checkTargetTypeDef(typeDef, targetClass);
+    }
+    return buildCheckedMetaSharedTypeInfo(typeDef);
   }
 
   private static TypeInfo getMetaReadTypeInfo(MetaReadContext metaReadContext, int index) {
@@ -923,6 +929,20 @@ public abstract class TypeResolver {
     return transformTypeInfo(typeInfo, targetClass, typeDefId);
   }
 
+  private void checkTargetTypeDef(TypeDef typeDef, Class<?> targetClass) {
+    Class<?> readClass = loadClass(typeDef.getClassSpec());
+    checkClassForDeserialization(readClass);
+    if (!typeDef.isStructSchemaKind()
+        && readClass != targetClass
+        && !targetClass.isAssignableFrom(readClass)) {
+      throw new ForyException(
+          "Remote metadata type "
+              + readClass.getName()
+              + " does not match expected type "
+              + targetClass.getName());
+    }
+  }
+
   private static long transformCacheTypeDefId(TypeInfo typeInfo) {
     TypeDef typeDef = typeInfo.getTypeDef();
     return typeDef == null ? 0 : typeDef.getId();
@@ -931,19 +951,25 @@ public abstract class TypeResolver {
   private TypeInfo transformTypeInfo(TypeInfo typeInfo, Class<?> targetClass, long typeDefId) {
     Class<?> readClass = typeInfo.getType();
     TypeInfo newTypeInfo;
+    // Keep assignable target matches cached here. Calling Class.isAssignableFrom for every
+    // collection element is a hot-path regression for wildcard/object element targets.
     if (targetClass.isAssignableFrom(readClass)) {
       newTypeInfo = typeInfo;
     } else {
+      TypeDef typeDef = typeInfo.getTypeDef();
+      if (typeDef == null || !typeDef.isStructSchemaKind()) {
+        throw new ForyException(
+            "Remote metadata type "
+                + readClass.getName()
+                + " does not match expected type "
+                + targetClass.getName());
+      }
       // similar to create serializer for `UnknownStruct`
       newTypeInfo =
-          getMetaSharedTypeInfo(
-              typeInfo.typeDef.replaceRootClassTo(this, targetClass), targetClass);
+          getMetaSharedTypeInfo(typeDef.replaceRootClassTo(this, targetClass), targetClass);
     }
     TransformedTypeInfo[] infos = extRegistry.transformedTypeInfo.get(targetClass);
     int size = infos == null ? 0 : infos.length;
-    if (size >= MAX_CACHED_TYPE_DEFS) {
-      return newTypeInfo;
-    }
     TransformedTypeInfo[] newInfos = new TransformedTypeInfo[size + 1];
     if (size > 0) {
       System.arraycopy(infos, 0, newInfos, 0, size);
@@ -1030,8 +1056,18 @@ public abstract class TypeResolver {
       return typeInfo;
     }
     Class<?> cls = loadClass(typeDef.getClassSpec());
-    // A wire TypeDef may create a compatible serializer; admit the class before caching it by id.
     checkClassForDeserialization(cls);
+    return buildMetaSharedTypeInfo(typeDef, cls);
+  }
+
+  private TypeInfo buildMetaSharedTypeInfo(TypeDef typeDef, Class<?> cls) {
+    TypeInfo typeInfo = createMetaSharedTypeInfo(typeDef, cls);
+    extRegistry.typeInfoByTypeDefId.put(typeDef.getId(), typeInfo);
+    return typeInfo;
+  }
+
+  private TypeInfo createMetaSharedTypeInfo(TypeDef typeDef, Class<?> cls) {
+    TypeInfo typeInfo;
     if (!typeDef.isStructSchemaKind()
         && !UnknownClass.class.isAssignableFrom(TypeUtils.getComponentIfArray(cls))) {
       typeInfo = getTypeInfo(cls);
@@ -1042,12 +1078,42 @@ public abstract class TypeResolver {
     } else {
       typeInfo = getMetaSharedTypeInfo(typeDef, cls);
     }
-    if (extRegistry.typeInfoByTypeDefId.size < MAX_CACHED_TYPE_DEFS) {
-      extRegistry.typeInfoByTypeDefId.put(typeDef.getId(), typeInfo);
-    }
     return typeInfo;
   }
 
+  private TypeInfo buildCheckedMetaSharedTypeInfo(TypeDef typeDef) {
+    Class<?> cls = loadClass(typeDef.getClassSpec());
+    // A wire TypeDef may create a compatible serializer; check the class before counting it.
+    checkClassForDeserialization(cls);
+    TypeDef localTypeDef = exactLocalTypeDef(typeDef, cls);
+    if (localTypeDef != null) {
+      return buildMetaSharedTypeInfo(localTypeDef, cls);
+    }
+    Object remoteTypeKey = remoteTypeKey(typeDef);
+    sharedRegistry.checkRemoteTypeDefLimit(typeDef, remoteTypeKey);
+    TypeInfo typeInfo = createMetaSharedTypeInfo(typeDef, cls);
+    TypeDef cachedTypeDef = sharedRegistry.getOrCreateRemoteTypeDef(typeDef, remoteTypeKey);
+    if (cachedTypeDef != typeDef) {
+      return buildMetaSharedTypeInfo(cachedTypeDef, cls);
+    }
+    extRegistry.typeInfoByTypeDefId.put(typeDef.getId(), typeInfo);
+    return typeInfo;
+  }
+
+  private TypeDef exactLocalTypeDef(TypeDef remoteTypeDef, Class<?> cls) {
+    return exactLocalTypeDef(remoteTypeDef.getEncoded(), cls);
+  }
+
+  private TypeDef exactLocalTypeDef(byte[] remoteEncoded, Class<?> cls) {
+    // UnknownStruct has no local metadata to compare against; building it would change
+    // open-world unknown-type semantics.
+    if (UnknownClass.class.isAssignableFrom(TypeUtils.getComponentIfArray(cls))) {
+      return null;
+    }
+    TypeDef localTypeDef = getTypeDef(cls, true);
+    return Arrays.equals(remoteEncoded, localTypeDef.getEncoded()) ? localTypeDef : null;
+  }
+
   // TODO(chaokunyang) if TypeDef is consistent with class in this process,
   //  use existing serializer instead.
   private TypeInfo getMetaSharedTypeInfo(TypeDef typeDef, Class<?> clz) {
@@ -1276,10 +1342,14 @@ public abstract class TypeResolver {
 
   final Class<?> loadClass(
       String className, boolean isEnum, int arrayDims, boolean deserializeUnknownClass) {
+    // Remote TypeDef/TypeMeta paths reach class materialization through this owner. Keep
+    // name-level checks before Class.forName so rejected metadata cannot force arbitrary class
+    // loading.
     if (!extRegistry.typeChecker.checkType(this, className)) {
       throw new InsecureException(
           String.format("Class %s is forbidden for serialization.", className));
     }
+    DisallowedList.checkNotInDisallowedList(className);
     Class<?> cls = extRegistry.registeredClasses.get(className);
     if (cls != null) {
       return cls;
@@ -1470,6 +1540,18 @@ public abstract class TypeResolver {
     return sharedRegistry.getOrCreateTypeDef(typeDef);
   }
 
+  @Internal
+  public final TypeDef cacheRemoteTypeDef(TypeDef typeDef) {
+    return sharedRegistry.getOrCreateRemoteTypeDef(typeDef, remoteTypeKey(typeDef));
+  }
+
+  private static Object remoteTypeKey(TypeDef typeDef) {
+    if (typeDef.isNamed()) {
+      return typeDef.getClassSpec().entireClassName;
+    }
+    return typeDef.getClassSpec().userTypeId;
+  }
+
   public final boolean isSerializable(Class<?> cls) {
     // Enums are always serializable, even if abstract (enums with abstract methods)
     if (cls.isEnum()) {
diff --git a/java/fory-core/src/main/java/org/apache/fory/serializer/ObjectStreamSerializer.java b/java/fory-core/src/main/java/org/apache/fory/serializer/ObjectStreamSerializer.java
index c0444c3b6..149e87286 100644
--- a/java/fory-core/src/main/java/org/apache/fory/serializer/ObjectStreamSerializer.java
+++ b/java/fory-core/src/main/java/org/apache/fory/serializer/ObjectStreamSerializer.java
@@ -93,7 +93,6 @@ import org.apache.fory.util.Preconditions;
 @SuppressWarnings({"unchecked", "rawtypes"})
 public class ObjectStreamSerializer extends AbstractObjectSerializer {
   private static final Logger LOG = LoggerFactory.getLogger(ObjectStreamSerializer.class);
-  private static final int MAX_CACHED_TYPE_DEFS = 8192;
 
   private final SlotInfo[] slotsInfos;
   // Instance-level cache: TypeDef ID -> TypeInfo (shared across all slots).
@@ -279,7 +278,7 @@ public class ObjectStreamSerializer extends AbstractObjectSerializer {
       ClassResolver classResolver = (ClassResolver) typeResolver;
       TreeMap<Integer, ObjectInputValidation> callbacks = new TreeMap<>(Collections.reverseOrder());
       for (int i = 0; i < numClasses; i++) {
-        // Matching layers are admitted by the registered root object type; requiring each
+        // Matching layers are accepted by the registered root object type; requiring each
         // serializable superclass to be registered would make normal ObjectStream hierarchy reads
         // unusable. Sender-only layers are checked below before their data is skipped.
         Class<?> currentClass = classResolver.readClassInternalUnchecked(readContext);
@@ -553,12 +552,25 @@ public class ObjectStreamSerializer extends AbstractObjectSerializer {
       TypeDef.skipTypeDef(buffer, typeDefId);
       return typeInfo;
     }
-    TypeDef typeDef =
-        typeResolver.cacheTypeDef(TypeDef.readTypeDef(typeResolver, buffer, typeDefId));
-    typeInfo = new TypeInfo(cls, typeDef);
-    if (typeDefIdToTypeInfo.size < MAX_CACHED_TYPE_DEFS) {
-      typeDefIdToTypeInfo.put(typeDefId, typeInfo);
+    return readLayerTypeDef(typeResolver, buffer, cls, typeDefId);
+  }
+
+  private TypeInfo readLayerTypeDef(
+      TypeResolver typeResolver, MemoryBuffer buffer, Class<?> cls, long typeDefId) {
+    byte[] encoded = TypeDef.readTypeDefBytes(typeResolver, buffer, typeDefId);
+    TypeDef localTypeDef = typeResolver.getTypeDef(cls, false);
+    TypeDef typeDef;
+    if (Arrays.equals(encoded, localTypeDef.getEncoded())) {
+      // Exact local bytes only avoid remote schema counting/parsing. They still select the
+      // target class serializer, so the class must pass the active deserialization policy.
+      typeResolver.checkClassForDeserialization(cls);
+      typeDef = localTypeDef;
+    } else {
+      typeResolver.checkClassForDeserialization(cls);
+      typeDef = typeResolver.cacheRemoteTypeDef(TypeDef.readTypeDef(typeResolver, encoded));
     }
+    TypeInfo typeInfo = new TypeInfo(cls, typeDef);
+    typeDefIdToTypeInfo.put(typeDefId, typeInfo);
     return typeInfo;
   }
 
diff --git a/java/fory-core/src/test/java/org/apache/fory/meta/NativeTypeDefEncoderTest.java b/java/fory-core/src/test/java/org/apache/fory/meta/NativeTypeDefEncoderTest.java
index 2d6bbce13..fc6875c9c 100644
--- a/java/fory-core/src/test/java/org/apache/fory/meta/NativeTypeDefEncoderTest.java
+++ b/java/fory-core/src/test/java/org/apache/fory/meta/NativeTypeDefEncoderTest.java
@@ -227,6 +227,49 @@ public class NativeTypeDefEncoderTest {
     Assert.assertEquals(header & TypeDef.COMPRESS_META_FLAG, TypeDef.COMPRESS_META_FLAG);
   }
 
+  @Test
+  public void testDecodeRejectsTooManyFields() {
+    Fory writer = Fory.builder().withXlang(false).withMetaShare(true).withCompatible(false).build();
+    TypeDef typeDef =
+        TypeDef.buildTypeDef(writer.getTypeResolver(), TypeDefEncoderTest.ManyFields.class);
+    Fory reader =
+        Fory.builder()
+            .withXlang(false)
+            .withMetaShare(true)
+            .withCompatible(false)
+            .withMaxTypeFields(31)
+            .build();
+
+    DeserializationException exception =
+        Assert.expectThrows(
+            DeserializationException.class,
+            () ->
+                TypeDef.readTypeDef(
+                    reader.getTypeResolver(), MemoryBuffer.fromByteArray(typeDef.getEncoded())));
+    Assert.assertTrue(exception.getMessage().contains("maxTypeFields"));
+  }
+
+  @Test
+  public void testDecodeRejectsTypeMetaBodySize() {
+    Fory writer = Fory.builder().withXlang(false).withMetaShare(true).withCompatible(false).build();
+    TypeDef typeDef = TypeDef.buildTypeDef(writer.getTypeResolver(), Foo1.class);
+    Fory reader =
+        Fory.builder()
+            .withXlang(false)
+            .withMetaShare(true)
+            .withCompatible(false)
+            .withMaxTypeMetaBytes(1)
+            .build();
+
+    DeserializationException exception =
+        Assert.expectThrows(
+            DeserializationException.class,
+            () ->
+                TypeDef.readTypeDef(
+                    reader.getTypeResolver(), MemoryBuffer.fromByteArray(typeDef.getEncoded())));
+    Assert.assertTrue(exception.getMessage().contains("maxTypeMetaBytes"));
+  }
+
   @Test
   public void testDecodeRejectsReservedGlobalBits() {
     Fory fory = Fory.builder().withXlang(false).withMetaShare(true).withCompatible(false).build();
diff --git a/java/fory-core/src/test/java/org/apache/fory/meta/TypeDefEncoderTest.java b/java/fory-core/src/test/java/org/apache/fory/meta/TypeDefEncoderTest.java
index 613d50342..3221d176a 100644
--- a/java/fory-core/src/test/java/org/apache/fory/meta/TypeDefEncoderTest.java
+++ b/java/fory-core/src/test/java/org/apache/fory/meta/TypeDefEncoderTest.java
@@ -581,6 +581,50 @@ public class TypeDefEncoderTest {
     Assert.assertEquals(decoded.getFieldsInfo().size(), 32);
   }
 
+  @Test
+  public void testDecodeRejectsTooManyFields() {
+    Fory writer = Fory.builder().withXlang(true).withCompatible(false).withMetaShare(true).build();
+    writer.register(ManyFields.class, 6002);
+    TypeDef typeDef = TypeDef.buildTypeDef(writer.getTypeResolver(), ManyFields.class);
+    Fory reader =
+        Fory.builder()
+            .withXlang(true)
+            .withCompatible(false)
+            .withMetaShare(true)
+            .withMaxTypeFields(31)
+            .build();
+
+    DeserializationException exception =
+        Assert.expectThrows(
+            DeserializationException.class,
+            () ->
+                TypeDef.readTypeDef(
+                    reader.getTypeResolver(), MemoryBuffer.fromByteArray(typeDef.getEncoded())));
+    Assert.assertTrue(exception.getMessage().contains("maxTypeFields"));
+  }
+
+  @Test
+  public void testDecodeRejectsTypeMetaBodySize() {
+    Fory writer = Fory.builder().withXlang(true).withCompatible(false).withMetaShare(true).build();
+    writer.register(ClassWithNoAnnotations.class, 6003);
+    TypeDef typeDef = TypeDef.buildTypeDef(writer.getTypeResolver(), ClassWithNoAnnotations.class);
+    Fory reader =
+        Fory.builder()
+            .withXlang(true)
+            .withCompatible(false)
+            .withMetaShare(true)
+            .withMaxTypeMetaBytes(1)
+            .build();
+
+    DeserializationException exception =
+        Assert.expectThrows(
+            DeserializationException.class,
+            () ->
+                TypeDef.readTypeDef(
+                    reader.getTypeResolver(), MemoryBuffer.fromByteArray(typeDef.getEncoded())));
+    Assert.assertTrue(exception.getMessage().contains("maxTypeMetaBytes"));
+  }
+
   @Test
   public void testDecodeRejectsCompressedXlangTypeDef() {
     Fory fory = Fory.builder().withXlang(true).withCompatible(false).withMetaShare(true).build();
diff --git a/java/fory-core/src/test/java/org/apache/fory/resolver/ClassResolverTest.java b/java/fory-core/src/test/java/org/apache/fory/resolver/ClassResolverTest.java
index 1dd0cbdde..0b94da34c 100644
--- a/java/fory-core/src/test/java/org/apache/fory/resolver/ClassResolverTest.java
+++ b/java/fory-core/src/test/java/org/apache/fory/resolver/ClassResolverTest.java
@@ -51,8 +51,10 @@ import org.apache.fory.Fory;
 import org.apache.fory.ForyTestBase;
 import org.apache.fory.builder.Generated;
 import org.apache.fory.config.ForyBuilder;
+import org.apache.fory.context.MetaReadContext;
 import org.apache.fory.context.ReadContext;
 import org.apache.fory.context.WriteContext;
+import org.apache.fory.exception.ForyException;
 import org.apache.fory.exception.InsecureException;
 import org.apache.fory.logging.Logger;
 import org.apache.fory.logging.LoggerFactory;
@@ -70,6 +72,7 @@ import org.apache.fory.serializer.Shareable;
 import org.apache.fory.serializer.collection.CollectionSerializer;
 import org.apache.fory.serializer.collection.CollectionSerializers;
 import org.apache.fory.serializer.collection.MapSerializers;
+import org.apache.fory.test.bean.BeanA;
 import org.apache.fory.test.bean.BeanB;
 import org.apache.fory.type.Descriptor;
 import org.apache.fory.type.DescriptorGrouper;
@@ -341,31 +344,220 @@ public class ClassResolverTest extends ForyTestBase {
   }
 
   @Test
-  public void testTypeDefHeaderCacheStopsAtMaxEntries() {
+  public void testRemoteTypeDefChecksTypeChecker() {
+    Fory reader =
+        Fory.builder()
+            .withXlang(false)
+            .requireClassRegistration(false)
+            .withCompatible(false)
+            .withMetaShare(true)
+            .build();
+    reader
+        .getTypeResolver()
+        .setTypeChecker((resolver, className) -> !className.equals(BeanB.class.getName()));
+    ClassResolver resolver = (ClassResolver) reader.getTypeResolver();
+    TypeDef typeDef = resolver.getTypeDef(BeanB.class, true);
+    ReadContext readContext = reader.getReadContext();
+    readContext.setMetaReadContext(new MetaReadContext());
+    MemoryBuffer buffer = MemoryBuffer.newHeapBuffer(256);
+    readContext.prepare(buffer, null, false);
+    buffer.writeVarUInt32(0);
+    typeDef.writeTypeDef(buffer);
+    buffer.readerIndex(0);
+
+    Assert.assertThrows(
+        InsecureException.class, () -> resolver.readSharedClassMeta(readContext, BeanB.class));
+  }
+
+  @Test
+  public void testMetaLoadClassChecksDisallowedList() {
+    String className = ProcessBuilder.class.getName();
+    CountingClassLoader classLoader =
+        new CountingClassLoader(ClassResolverTest.class.getClassLoader(), className);
+    Fory reader =
+        Fory.builder()
+            .withXlang(false)
+            .requireClassRegistration(false)
+            .withCompatible(false)
+            .withMetaShare(true)
+            .withClassLoader(classLoader)
+            .build();
+    ClassResolver resolver = (ClassResolver) reader.getTypeResolver();
+
+    Assert.assertThrows(
+        InsecureException.class, () -> resolver.loadClassForMeta(className, false, -1));
+    assertEquals(classLoader.loadCount, 0);
+  }
+
+  @Test
+  public void testRemoteSchemaVersionLimitByType() {
     ForyBuilder builder =
         Fory.builder()
             .withXlang(false)
             .requireClassRegistration(false)
             .withCompatible(false)
-            .withMetaShare(true);
+            .withMetaShare(true)
+            .withMaxSchemaVersionsPerType(1);
     finishBuilder(builder);
     SharedRegistry sharedRegistry = new SharedRegistry();
     Fory fory = new Fory(builder, ClassResolverTest.class.getClassLoader(), sharedRegistry);
     ClassResolver resolver = (ClassResolver) fory.getTypeResolver();
-    TypeDef typeDef = TypeDef.buildTypeDef(resolver, BeanB.class);
-    int maxCachedTypeDefs = 8192;
-    for (long i = 0; i < maxCachedTypeDefs; i++) {
-      sharedRegistry.typeDefById.put(i, typeDef);
-    }
+    TypeDef first = TypeDef.buildTypeDef(resolver, BeanB.class);
+    TypeDef second = TypeDef.buildTypeDef(resolver, BeanA.class);
+
+    assertSame(first, sharedRegistry.getOrCreateRemoteTypeDef(first, "remote.Type"));
+    assertSame(first, sharedRegistry.getOrCreateRemoteTypeDef(first, "remote.Type"));
+    Assert.assertThrows(
+        ForyException.class, () -> sharedRegistry.getOrCreateRemoteTypeDef(second, "remote.Type"));
+  }
+
+  @Test
+  public void testSharedRegistryRemoteSchemaLimitInit() {
+    SharedRegistry sharedRegistry = new SharedRegistry();
 
+    Assert.assertThrows(
+        IllegalArgumentException.class, () -> sharedRegistry.setRemoteSchemaLimits(0, 3));
+    Assert.assertThrows(
+        IllegalArgumentException.class, () -> sharedRegistry.setRemoteSchemaLimits(10, 0));
+
+    sharedRegistry.setRemoteSchemaLimits(10, 3);
+    sharedRegistry.setRemoteSchemaLimits(10, 3);
+    Assert.assertThrows(
+        IllegalArgumentException.class, () -> sharedRegistry.setRemoteSchemaLimits(11, 3));
+    Assert.assertThrows(
+        IllegalArgumentException.class, () -> sharedRegistry.setRemoteSchemaLimits(10, 4));
+  }
+
+  @Test
+  public void testRemoteEnumTypeDefUsesLimit() {
+    ForyBuilder builder =
+        Fory.builder()
+            .withXlang(true)
+            .requireClassRegistration(true)
+            .withCompatible(false)
+            .withMetaShare(true)
+            .withMaxSchemaVersionsPerType(1);
+    finishBuilder(builder);
+    SharedRegistry sharedRegistry = new SharedRegistry();
+    Fory fory = new Fory(builder, ClassResolverTest.class.getClassLoader(), sharedRegistry);
+    fory.register(TestNeedToWriteReferenceClass.class, "test.Enum");
+    fory.register(OtherTestNeedToWriteReferenceClass.class, "test.OtherEnum");
+    TypeResolver resolver = fory.getTypeResolver();
+    TypeDef first = resolver.getTypeDef(TestNeedToWriteReferenceClass.class, true);
+    TypeDef second = TypeDef.buildTypeDef(resolver, OtherTestNeedToWriteReferenceClass.class);
+
+    Assert.assertFalse(first.isStructSchemaKind());
+    assertSame(first, sharedRegistry.getOrCreateRemoteTypeDef(first, "remote.Enum"));
+    Assert.assertThrows(
+        ForyException.class, () -> sharedRegistry.getOrCreateRemoteTypeDef(second, "remote.Enum"));
+  }
+
+  @Test
+  public void testExactLocalEnumTypeDefBypassesLimit() {
+    ForyBuilder builder =
+        Fory.builder()
+            .withXlang(true)
+            .requireClassRegistration(true)
+            .withCompatible(false)
+            .withMetaShare(true)
+            .withMaxSchemaVersionsPerType(1);
+    finishBuilder(builder);
+    SharedRegistry sharedRegistry = new SharedRegistry();
+    Fory fory = new Fory(builder, ClassResolverTest.class.getClassLoader(), sharedRegistry);
+    fory.register(TestNeedToWriteReferenceClass.class, "test.Enum");
+    fory.register(OtherTestNeedToWriteReferenceClass.class, "test.OtherEnum");
+    TypeResolver resolver = fory.getTypeResolver();
+    TypeDef exact = resolver.getTypeDef(TestNeedToWriteReferenceClass.class, true);
+    TypeDef other = TypeDef.buildTypeDef(resolver, OtherTestNeedToWriteReferenceClass.class);
+
+    ReadContext readContext = fory.getReadContext();
+    readContext.setMetaReadContext(new MetaReadContext());
     MemoryBuffer buffer = MemoryBuffer.newHeapBuffer(256);
-    typeDef.writeTypeDef(buffer);
+    readContext.prepare(buffer, null, false);
+    buffer.writeVarUInt32(0);
+    exact.writeTypeDef(buffer);
     buffer.readerIndex(0);
-    TypeDef readTypeDef = resolver.readTypeDef(buffer, buffer.readInt64());
 
-    assertNotNull(readTypeDef);
-    assertNull(sharedRegistry.typeDefById.get(typeDef.getId()));
-    assertEquals(sharedRegistry.typeDefById.size(), maxCachedTypeDefs);
+    assertSame(
+        resolver.readSharedClassMeta(readContext, TestNeedToWriteReferenceClass.class).getType(),
+        TestNeedToWriteReferenceClass.class);
+    assertSame(other, sharedRegistry.getOrCreateRemoteTypeDef(other, "test.Enum"));
+  }
+
+  @Test
+  public void testIdEnumDoesNotUseTypeDefMetaLimits() {
+    Fory fory =
+        Fory.builder()
+            .withXlang(true)
+            .requireClassRegistration(true)
+            .withCompatible(true)
+            .withMaxTypeMetaBytes(1)
+            .withMaxSchemaVersionsPerType(1)
+            .build();
+    fory.register(TestNeedToWriteReferenceClass.class, 101);
+
+    serDeCheck(fory, TestNeedToWriteReferenceClass.A);
+  }
+
+  @Test
+  public void testIdExtDoesNotUseTypeDefMetaLimits() {
+    Fory fory =
+        Fory.builder()
+            .withXlang(true)
+            .requireClassRegistration(true)
+            .withCompatible(true)
+            .withMaxTypeMetaBytes(1)
+            .withMaxSchemaVersionsPerType(1)
+            .build();
+    fory.register(IdLimitExt.class, 102);
+    fory.registerSerializer(IdLimitExt.class, IdLimitExtSerializer.class);
+
+    IdLimitExt value = new IdLimitExt();
+    value.value = 42;
+    serDeCheck(fory, value);
+    assertEquals(fory.getTypeResolver().getTypeInfo(IdLimitExt.class).getTypeId(), Types.EXT);
+  }
+
+  @Test
+  public void testRemoteSchemaVersionsUseRemoteTypeKey() {
+    ForyBuilder builder =
+        Fory.builder()
+            .withXlang(false)
+            .requireClassRegistration(false)
+            .withCompatible(false)
+            .withMetaShare(true)
+            .withMaxSchemaVersionsPerType(1);
+    finishBuilder(builder);
+    SharedRegistry sharedRegistry = new SharedRegistry();
+    Fory fory = new Fory(builder, ClassResolverTest.class.getClassLoader(), sharedRegistry);
+    ClassResolver resolver = (ClassResolver) fory.getTypeResolver();
+
+    TypeDef first = TypeDef.buildTypeDef(resolver, BeanB.class);
+    TypeDef second = TypeDef.buildTypeDef(resolver, BeanA.class);
+
+    assertSame(first, sharedRegistry.getOrCreateRemoteTypeDef(first, "remote.UnknownA"));
+    assertSame(second, sharedRegistry.getOrCreateRemoteTypeDef(second, "remote.UnknownB"));
+  }
+
+  @Test
+  public void testRemoteTypeDefCheckOnly() {
+    ForyBuilder builder =
+        Fory.builder()
+            .withXlang(false)
+            .requireClassRegistration(false)
+            .withCompatible(false)
+            .withMetaShare(true)
+            .withMaxSchemaVersionsPerType(1);
+    finishBuilder(builder);
+    SharedRegistry sharedRegistry = new SharedRegistry();
+    Fory fory = new Fory(builder, ClassResolverTest.class.getClassLoader(), sharedRegistry);
+    ClassResolver resolver = (ClassResolver) fory.getTypeResolver();
+
+    TypeDef checked = TypeDef.buildTypeDef(resolver, BeanB.class);
+    TypeDef accepted = TypeDef.buildTypeDef(resolver, BeanA.class);
+
+    sharedRegistry.checkRemoteTypeDefLimit(checked, "remote.Type");
+    assertSame(accepted, sharedRegistry.getOrCreateRemoteTypeDef(accepted, "remote.Type"));
   }
 
   @Test
@@ -490,6 +682,24 @@ public class ClassResolverTest extends ForyTestBase {
     }
   }
 
+  private static final class CountingClassLoader extends ClassLoader {
+    private final String countedClassName;
+    private int loadCount;
+
+    private CountingClassLoader(ClassLoader parent, String countedClassName) {
+      super(parent);
+      this.countedClassName = countedClassName;
+    }
+
+    @Override
+    protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
+      if (countedClassName.equals(name)) {
+        loadCount++;
+      }
+      return super.loadClass(name, resolve);
+    }
+  }
+
   interface Interface1 {}
 
   interface Interface2 {}
@@ -627,6 +837,11 @@ public class ClassResolverTest extends ForyTestBase {
     int f1;
   }
 
+  @Data
+  static class IdLimitExt {
+    int value;
+  }
+
   @EqualsAndHashCode(callSuper = true)
   @ToString
   static class Bar extends Foo {
@@ -644,6 +859,29 @@ public class ClassResolverTest extends ForyTestBase {
     B
   }
 
+  private enum OtherTestNeedToWriteReferenceClass {
+    C,
+    D
+  }
+
+  public static class IdLimitExtSerializer extends Serializer<IdLimitExt> {
+    public IdLimitExtSerializer(TypeResolver typeResolver, Class<IdLimitExt> type) {
+      super(typeResolver.getConfig(), type);
+    }
+
+    @Override
+    public void write(WriteContext writeContext, IdLimitExt value) {
+      writeContext.getBuffer().writeInt32(value.value);
+    }
+
+    @Override
+    public IdLimitExt read(ReadContext readContext) {
+      IdLimitExt value = new IdLimitExt();
+      value.value = readContext.getBuffer().readInt32();
+      return value;
+    }
+  }
+
   @Test
   public void testNeedToWriteReference() {
     Fory fory =
diff --git a/java/fory-core/src/test/java/org/apache/fory/resolver/MetaShareContextTest.java b/java/fory-core/src/test/java/org/apache/fory/resolver/MetaShareContextTest.java
index 87a5d139c..004852b4c 100644
--- a/java/fory-core/src/test/java/org/apache/fory/resolver/MetaShareContextTest.java
+++ b/java/fory-core/src/test/java/org/apache/fory/resolver/MetaShareContextTest.java
@@ -98,7 +98,7 @@ public class MetaShareContextTest extends ForyTestBase {
   }
 
   @Test
-  public void testMetaTypeDefAdmission() {
+  public void testMetaTypeDefCheck() {
     Fory writer =
         Fory.builder()
             .withXlang(false)
diff --git a/java/fory-core/src/test/java/org/apache/fory/resolver/MetaStringIOTest.java b/java/fory-core/src/test/java/org/apache/fory/resolver/MetaStringIOTest.java
index ea1bb16fa..eb07e8606 100644
--- a/java/fory-core/src/test/java/org/apache/fory/resolver/MetaStringIOTest.java
+++ b/java/fory-core/src/test/java/org/apache/fory/resolver/MetaStringIOTest.java
@@ -40,9 +40,13 @@ import org.apache.fory.util.StringUtils;
 import org.testng.annotations.Test;
 
 public class MetaStringIOTest {
+  private static SharedRegistry newSharedRegistry() {
+    return new SharedRegistry();
+  }
+
   @Test
   public void testWriteMetaString() {
-    SharedRegistry sharedRegistry = new SharedRegistry();
+    SharedRegistry sharedRegistry = newSharedRegistry();
     MetaStringWriter writer = new MetaStringWriter();
     MetaStringReader reader = new MetaStringReader(sharedRegistry);
     MemoryBuffer buffer = MemoryUtils.buffer(32);
@@ -67,7 +71,7 @@ public class MetaStringIOTest {
         }) {
       for (int i = 0; i < 32; i++) {
         String str = StringUtils.random(i, 0);
-        SharedRegistry sharedRegistry = new SharedRegistry();
+        SharedRegistry sharedRegistry = newSharedRegistry();
         MetaStringWriter writer = new MetaStringWriter();
         MetaStringReader reader = new MetaStringReader(sharedRegistry);
         writer.writeMetaString(buffer, newGenericMetaString(str));
@@ -82,7 +86,7 @@ public class MetaStringIOTest {
 
   @Test
   public void testMetaStringWriterResetClearsDynamicWriteState() {
-    SharedRegistry sharedRegistry = new SharedRegistry();
+    SharedRegistry sharedRegistry = newSharedRegistry();
     MetaStringWriter writer = new MetaStringWriter();
     MetaStringReader reader = new MetaStringReader(sharedRegistry);
     EncodedMetaString metaString = newGenericMetaString("thread_safe_fory");
@@ -101,7 +105,7 @@ public class MetaStringIOTest {
 
   @Test
   public void testMetaStringReaderUsesSharedRegistryInstances() {
-    SharedRegistry sharedRegistry = new SharedRegistry();
+    SharedRegistry sharedRegistry = newSharedRegistry();
     MetaStringWriter writer = new MetaStringWriter();
     MetaStringReader reader = new MetaStringReader(sharedRegistry);
     EncodedMetaString encodedMetaString = newGenericMetaString("shared_meta_string");
@@ -119,7 +123,7 @@ public class MetaStringIOTest {
 
   @Test
   public void testSharedRegistrySkipsLongEncodedMetaStrings() {
-    SharedRegistry sharedRegistry = new SharedRegistry();
+    SharedRegistry sharedRegistry = newSharedRegistry();
     String str = StringUtils.random(2050, 0);
 
     EncodedMetaString first = newGenericMetaString(sharedRegistry, str);
@@ -130,7 +134,7 @@ public class MetaStringIOTest {
 
   @Test
   public void testSharedRegistryCapsEncodedMetaStringCount() {
-    SharedRegistry sharedRegistry = new SharedRegistry();
+    SharedRegistry sharedRegistry = newSharedRegistry();
     EncodedMetaString first = null;
     for (int i = 0; i < 32768; i++) {
       EncodedMetaString encodedMetaString = newGenericMetaString(sharedRegistry, "meta_" + i);
@@ -148,7 +152,7 @@ public class MetaStringIOTest {
 
   @Test
   public void testReadBigMetaStringRejectsNonCanonicalHash() {
-    SharedRegistry sharedRegistry = new SharedRegistry();
+    SharedRegistry sharedRegistry = newSharedRegistry();
     MetaStringReader reader = new MetaStringReader(sharedRegistry);
     EncodedMetaString encodedMetaString = newGenericMetaString(StringUtils.random(32, 0));
     MemoryBuffer buffer = MemoryUtils.buffer(64);
@@ -162,7 +166,7 @@ public class MetaStringIOTest {
 
   @Test
   public void testCachedBigMetaStringReusesHeaderCache() {
-    SharedRegistry sharedRegistry = new SharedRegistry();
+    SharedRegistry sharedRegistry = newSharedRegistry();
     MetaStringReader reader = new MetaStringReader(sharedRegistry);
     EncodedMetaString encodedMetaString = newGenericMetaString(StringUtils.random(32, 0));
     MemoryBuffer buffer = MemoryUtils.buffer(128);
@@ -179,7 +183,7 @@ public class MetaStringIOTest {
 
   @Test
   public void testReadSmallMetaStringKeyIncludesLengthAndEncoding() {
-    SharedRegistry sharedRegistry = new SharedRegistry();
+    SharedRegistry sharedRegistry = newSharedRegistry();
     MetaStringReader reader = new MetaStringReader(sharedRegistry);
     MemoryBuffer buffer = MemoryUtils.buffer(32);
 
@@ -201,7 +205,7 @@ public class MetaStringIOTest {
 
   @Test
   public void testMetaStringReaderResetClearsDynamicIdsOnly() {
-    SharedRegistry sharedRegistry = new SharedRegistry();
+    SharedRegistry sharedRegistry = newSharedRegistry();
     MetaStringReader reader = new MetaStringReader(sharedRegistry);
     MemoryBuffer buffer = MemoryUtils.buffer(32);
 
diff --git a/java/fory-core/src/test/java/org/apache/fory/serializer/LambdaSerializerTest.java b/java/fory-core/src/test/java/org/apache/fory/serializer/LambdaSerializerTest.java
index ae40bdb81..6afd7c2fa 100644
--- a/java/fory-core/src/test/java/org/apache/fory/serializer/LambdaSerializerTest.java
+++ b/java/fory-core/src/test/java/org/apache/fory/serializer/LambdaSerializerTest.java
@@ -113,7 +113,7 @@ public class LambdaSerializerTest extends ForyTestBase {
   }
 
   @Test
-  public void testSerializedLambdaAdmission() throws Exception {
+  public void testSerializedLambdaClassCheck() throws Exception {
     int delta = 7;
     Function<Integer, Integer> function =
         (Serializable & Function<Integer, Integer>) (x) -> x + delta;
diff --git a/java/fory-core/src/test/java/org/apache/fory/serializer/ObjectStreamSerializerTest.java b/java/fory-core/src/test/java/org/apache/fory/serializer/ObjectStreamSerializerTest.java
index 933bd07c5..330b1ee99 100644
--- a/java/fory-core/src/test/java/org/apache/fory/serializer/ObjectStreamSerializerTest.java
+++ b/java/fory-core/src/test/java/org/apache/fory/serializer/ObjectStreamSerializerTest.java
@@ -1378,6 +1378,39 @@ public class ObjectStreamSerializerTest extends ForyTestBase {
     assertEquals(result2.value, 7);
   }
 
+  @Test(dataProvider = "compatibleModeProvider")
+  public void testObjectStreamExactLocalTypeDefChecksTypeChecker(boolean compatible) {
+    ForyBuilder builder =
+        Fory.builder()
+            .withXlang(false)
+            .requireClassRegistration(false)
+            .withRefTracking(true)
+            .withCompatible(compatible)
+            .withMetaShare(true);
+    finishBuilder(builder);
+    SharedRegistry sharedRegistry = new SharedRegistry();
+    Fory writerFory =
+        new Fory(builder, ObjectStreamSerializerTest.class.getClassLoader(), sharedRegistry);
+    Fory readerFory =
+        new Fory(builder, ObjectStreamSerializerTest.class.getClassLoader(), sharedRegistry);
+    writerFory.registerSerializer(
+        MixedSerializationClass.class,
+        new ObjectStreamSerializer(writerFory.getTypeResolver(), MixedSerializationClass.class));
+    readerFory.registerSerializer(
+        MixedSerializationClass.class,
+        new ObjectStreamSerializer(readerFory.getTypeResolver(), MixedSerializationClass.class));
+
+    writerFory.setMetaWriteContext(new MetaWriteContext());
+    byte[] bytes = writerFory.serialize(new MixedSerializationClass("blocked", 11));
+    readerFory
+        .getTypeResolver()
+        .setTypeChecker(
+            (resolver, className) -> !className.equals(MixedSerializationClass.class.getName()));
+    readerFory.setMetaReadContext(new MetaReadContext());
+
+    Assert.assertThrows(InsecureException.class, () -> readerFory.deserialize(bytes));
+  }
+
   // ==================== Default Value Tests ====================
 
   /** Class to test default values when fields are missing. */
diff --git a/java/fory-core/src/test/java/org/apache/fory/serializer/UnionSerializerTest.java b/java/fory-core/src/test/java/org/apache/fory/serializer/UnionSerializerTest.java
index c3175d8d6..b0b2926de 100644
--- a/java/fory-core/src/test/java/org/apache/fory/serializer/UnionSerializerTest.java
+++ b/java/fory-core/src/test/java/org/apache/fory/serializer/UnionSerializerTest.java
@@ -138,6 +138,25 @@ public class UnionSerializerTest extends ForyTestBase {
     assertEquals(union.getValue(), "hello");
   }
 
+  @Test
+  public void testIdUnionDoesNotUseTypeDefMetaLimits() {
+    Fory fory =
+        Fory.builder()
+            .withXlang(true)
+            .requireClassRegistration(true)
+            .withCompatible(true)
+            .withMaxTypeMetaBytes(1)
+            .withMaxSchemaVersionsPerType(1)
+            .build();
+    UnionSerializer serializer = new UnionSerializer(fory.getTypeResolver(), SchemaUnion.class);
+    fory.registerUnion(SchemaUnion.class, 107, serializer);
+
+    Union union = writeReadUnion(fory, serializer, new SchemaUnion(0, "hello", Types.STRING), 0);
+    assertTrue(union instanceof SchemaUnion);
+    assertEquals(union.getIndex(), 0);
+    assertEquals(union.getValue(), "hello");
+  }
+
   @Test
   public void testRegisterUnionDottedName() {
     Fory fory =
diff --git a/javascript/packages/core/lib/context.ts b/javascript/packages/core/lib/context.ts
index 466d50ece..bccde267e 100644
--- a/javascript/packages/core/lib/context.ts
+++ b/javascript/packages/core/lib/context.ts
@@ -530,23 +530,17 @@ export class WriteContext {
 }
 
 export class ReadContext {
-  private static readonly MAX_CACHED_TYPE_META = 8192;
-  private static readonly MAX_CACHED_COMPATIBLE_READ_SERIALIZER = 8192;
+  private static readonly MIN_REMOTE_TYPE_META_LIMIT = 8192;
 
   readonly reader: BinaryReader;
   readonly refReader: RefReader;
   readonly metaStringReader: MetaStringReader;
 
   private typeMeta: TypeMeta[] = [];
-  /** Persistent cross-message cache keyed by 8-byte type meta header. */
-  private typeMetaCache: Map<number, Map<number, TypeMeta>> = new Map();
-  private typeMetaCacheSize = 0;
-  private lastTypeMetaHeaderLow = -1;
-  private lastTypeMetaHeaderHigh = -1;
-  private lastTypeMeta: TypeMeta | null = null;
-  private recentTypeMetaHeaderLows = [-1, -1, -1, -1];
-  private recentTypeMetaHeaderHighs = [-1, -1, -1, -1];
-  private recentTypeMetas: Array<TypeMeta | null> = [null, null, null, null];
+  /** Persistent checked cache keyed by the 52-bit TypeMeta header hash. */
+  private typeMetaCache: Map<number, TypeMeta> = new Map();
+  private totalAcceptedSchemaVersions = 0;
+  private cachedTypeMeta: TypeMeta | undefined;
   private compatibleReadSerializers = new Map<
     number,
     CompatibleReadSerializerCacheEntry
@@ -554,52 +548,8 @@ export class ReadContext {
 
   private _depth = 0;
   private _maxDepth: number;
-
-  private static typeMetaHeaderHash(headerLow: number, headerHigh: number) {
-    return headerHigh * 0x100000 + (headerLow >>> 12);
-  }
-
-  private findRecentTypeMeta(
-    headerLow: number,
-    headerHigh: number,
-  ): TypeMeta | null {
-    const lows = this.recentTypeMetaHeaderLows;
-    const highs = this.recentTypeMetaHeaderHighs;
-    const metas = this.recentTypeMetas;
-    for (let i = 0; i < metas.length; i++) {
-      const typeMeta = metas[i];
-      if (
-        typeMeta !== null
-        && lows[i] === headerLow
-        && highs[i] === headerHigh
-      ) {
-        return typeMeta;
-      }
-    }
-    return null;
-  }
-
-  private rememberRecentTypeMeta(
-    headerLow: number,
-    headerHigh: number,
-    typeMeta: TypeMeta,
-  ) {
-    const lows = this.recentTypeMetaHeaderLows;
-    const highs = this.recentTypeMetaHeaderHighs;
-    const metas = this.recentTypeMetas;
-    if (lows[0] === headerLow && highs[0] === headerHigh) {
-      metas[0] = typeMeta;
-      return;
-    }
-    for (let i = metas.length - 1; i > 0; i--) {
-      lows[i] = lows[i - 1];
-      highs[i] = highs[i - 1];
-      metas[i] = metas[i - 1];
-    }
-    lows[0] = headerLow;
-    highs[0] = headerHigh;
-    metas[0] = typeMeta;
-  }
+  private remoteSchemaVersionsByType: Map<string | number, number> | undefined
+    = undefined;
 
   constructor(
     readonly typeResolver: TypeResolverLike,
@@ -649,80 +599,119 @@ export class ReadContext {
     this.refReader.reference(object);
   }
 
-  private readTypeMetaFromHeader(
-    dynamicTypeId: number,
-    headerLow: number,
-    headerHigh: number,
-  ): TypeMeta {
-    if (
-      this.lastTypeMeta !== null
-      && this.lastTypeMetaHeaderLow === headerLow
-      && this.lastTypeMetaHeaderHigh === headerHigh
-    ) {
-      TypeMeta.skipBodyByHeaderLow(this.reader, headerLow);
-      this.typeMeta[dynamicTypeId] = this.lastTypeMeta;
-      return this.lastTypeMeta;
-    }
+  private static typeMetaHeaderHash(headerLow: number, headerHigh: number) {
+    return headerHigh * 0x100000 + (headerLow >>> 12);
+  }
 
-    const recent = this.findRecentTypeMeta(headerLow, headerHigh);
-    if (recent !== null) {
-      TypeMeta.skipBodyByHeaderLow(this.reader, headerLow);
-      this.lastTypeMetaHeaderLow = headerLow;
-      this.lastTypeMetaHeaderHigh = headerHigh;
-      this.lastTypeMeta = recent;
-      this.typeMeta[dynamicTypeId] = recent;
-      return recent;
+  private findCachedTypeMeta(headerHash: number): TypeMeta | undefined {
+    const typeMeta = this.cachedTypeMeta;
+    if (typeMeta !== undefined && typeMeta.headerHash === headerHash) {
+      return typeMeta;
     }
+    return undefined;
+  }
 
-    const cached = this.typeMetaCache.get(headerHigh)?.get(headerLow);
-    let typeMeta: TypeMeta;
-    if (cached) {
-      // Header-cache hits intentionally skip without rehashing. Entries reach this cache only
-      // after a successful TypeMeta parse and 52-bit metadata-hash validation. The current body
-      // size still comes from the current header bytes, not from the cached TypeMeta.
-      TypeMeta.skipBodyByHeaderLow(this.reader, headerLow);
-      typeMeta = cached;
-      this.lastTypeMetaHeaderLow = headerLow;
-      this.lastTypeMetaHeaderHigh = headerHigh;
-      this.lastTypeMeta = typeMeta;
-      this.rememberRecentTypeMeta(headerLow, headerHigh, typeMeta);
-    } else {
-      const header = (BigInt(headerHigh) << 32n) | BigInt(headerLow);
-      typeMeta = TypeMeta.fromBytesAfterHeader(this.reader, header);
-      if (this.typeMetaCacheSize < ReadContext.MAX_CACHED_TYPE_META) {
-        let highCache = this.typeMetaCache.get(headerHigh);
-        if (highCache === undefined) {
-          highCache = new Map();
-          this.typeMetaCache.set(headerHigh, highCache);
-        }
-        highCache.set(headerLow, typeMeta);
-        this.typeMetaCacheSize++;
-        this.lastTypeMetaHeaderLow = headerLow;
-        this.lastTypeMetaHeaderHigh = headerHigh;
-        this.lastTypeMeta = typeMeta;
-        this.rememberRecentTypeMeta(headerLow, headerHigh, typeMeta);
-      }
-    }
-    this.typeMeta[dynamicTypeId] = typeMeta;
-    return typeMeta;
+  private rememberTypeMeta(typeMeta: TypeMeta) {
+    this.cachedTypeMeta = typeMeta;
   }
 
   readTypeMeta(): TypeMeta {
     const idOrLen = this.reader.readVarUInt32();
     if (idOrLen & 1) {
-      const typeMeta = this.typeMeta[idOrLen >> 1];
-      if (!typeMeta) {
-        throw new Error(`missing TypeMeta reference ${idOrLen >> 1}`);
-      }
+      return this.readTypeMetaRef(idOrLen);
+    }
+    const headerLow = this.reader.readUint32();
+    const headerHigh = this.reader.readUint32();
+    return this.readTypeMetaFromHeader(
+      idOrLen >> 1,
+      headerLow,
+      headerHigh,
+      ReadContext.typeMetaHeaderHash(headerLow, headerHigh),
+    );
+  }
+
+  readNamedTypeMeta(
+    expectedTypeId: number,
+    expectedNamespace: string,
+    expectedTypeName: string,
+  ): TypeMeta {
+    const idOrLen = this.reader.readVarUInt32();
+    if (idOrLen & 1) {
+      const typeMeta = this.readTypeMetaRef(idOrLen);
+      this.checkNamedTypeMeta(
+        typeMeta,
+        expectedTypeId,
+        expectedNamespace,
+        expectedTypeName,
+      );
       return typeMeta;
     }
+    const dynamicTypeId = idOrLen >> 1;
     const headerLow = this.reader.readUint32();
     const headerHigh = this.reader.readUint32();
-    return this.readTypeMetaFromHeader(idOrLen >> 1, headerLow, headerHigh);
+    const headerHash = ReadContext.typeMetaHeaderHash(headerLow, headerHigh);
+    let typeMeta: TypeMeta;
+    // Header-cache hits intentionally compare only TypeMeta.headerHash, then skip
+    // the body. That hash is the protocol metadata identity and is set only after
+    // body/hash validation. Do not add low-bit state, parallel header slots,
+    // rehashing, limits, exact-local checks, allocation, or policy work here;
+    // the miss path owns that.
+    const cachedTypeMeta = this.findCachedTypeMeta(headerHash);
+    if (cachedTypeMeta !== undefined) {
+      TypeMeta.skipBodyByHeaderLow(this.reader, headerLow);
+      typeMeta = cachedTypeMeta;
+    } else {
+      const cached = this.typeMetaCache.get(headerHash);
+      if (cached !== undefined && cached.headerHash === headerHash) {
+        TypeMeta.skipBodyByHeaderLow(this.reader, headerLow);
+        typeMeta = cached;
+        this.rememberTypeMeta(typeMeta);
+      } else {
+        const typeMetaStart = this.reader.readGetCursor() - 8;
+        const header = (BigInt(headerHigh) << 32n) | BigInt(headerLow);
+        typeMeta = TypeMeta.fromBytesAfterHeader(
+          this.reader,
+          header,
+          this.typeResolver.config.maxTypeFields,
+          this.typeResolver.config.maxTypeMetaBytes,
+        );
+        const typeMetaEnd = this.reader.readGetCursor();
+        this.checkNamedTypeMeta(
+          typeMeta,
+          expectedTypeId,
+          expectedNamespace,
+          expectedTypeName,
+        );
+        const localSerializer = this.serializerByTypeMeta(typeMeta);
+        if (localSerializer === undefined) {
+          throw new Error(
+            `can't find serializer for TypeMeta ${typeMeta.getNs()}$${typeMeta.getTypeName()}`,
+          );
+        }
+        if (
+          this.matchesExactLocalTypeMeta(typeMeta, typeMetaStart, typeMetaEnd)
+        ) {
+          this.cacheTypeMeta(headerHash, typeMeta, undefined);
+        } else {
+          const typeKey = this.checkRemoteTypeMetaLimit(typeMeta);
+          this.cacheTypeMeta(headerHash, typeMeta, typeKey);
+        }
+        this.typeMeta[dynamicTypeId] = typeMeta;
+        return typeMeta;
+      }
+    }
+    this.checkNamedTypeMeta(
+      typeMeta,
+      expectedTypeId,
+      expectedNamespace,
+      expectedTypeName,
+    );
+    this.typeMeta[dynamicTypeId] = typeMeta;
+    return typeMeta;
   }
 
-  readTypeMetaIfSchemaChanged(
-    expectedHash: number,
+  readCompatibleStructSerializer(
+    localHash: number,
     original?: Serializer,
   ): Serializer | undefined {
     const idOrLen = this.reader.readVarUInt32();
@@ -735,36 +724,252 @@ export class ReadContext {
       }
       remoteHash = typeMeta.getHash();
     } else {
+      const dynamicTypeId = idOrLen >> 1;
       const headerLow = this.reader.readUint32();
       const headerHigh = this.reader.readUint32();
+      const headerHash = ReadContext.typeMetaHeaderHash(headerLow, headerHigh);
       typeMeta = this.readTypeMetaFromHeader(
-        idOrLen >> 1,
+        dynamicTypeId,
         headerLow,
         headerHigh,
+        headerHash,
+        localHash,
+        original,
       );
-      remoteHash = ReadContext.typeMetaHeaderHash(headerLow, headerHigh);
+      remoteHash = headerHash;
     }
-    if (expectedHash !== remoteHash) {
-      const cached = this.compatibleReadSerializers.get(remoteHash);
-      if (cached !== undefined && cached.localHash === expectedHash) {
-        return cached.serializer;
-      }
-      const serializer = this.genSerializerByTypeMetaRuntime(
+    if (localHash !== remoteHash) {
+      return this.ensureCompatibleReadSerializer(
         typeMeta,
+        localHash,
+        remoteHash,
         original,
       );
+    }
+    return undefined;
+  }
+
+  private readTypeMetaRef(idOrLen: number): TypeMeta {
+    const refId = idOrLen >> 1;
+    const typeMeta = this.typeMeta[refId];
+    if (!typeMeta) {
+      throw new Error(`missing TypeMeta reference ${refId}`);
+    }
+    return typeMeta;
+  }
+
+  private checkNamedTypeMeta(
+    typeMeta: TypeMeta,
+    expectedTypeId: number,
+    expectedNamespace: string,
+    expectedTypeName: string,
+  ) {
+    if (
+      typeMeta.getTypeId() !== expectedTypeId
+      || typeMeta.getNs() !== expectedNamespace
+      || typeMeta.getTypeName() !== expectedTypeName
+    ) {
+      throw new Error(
+        `TypeMeta mismatch: expected ${expectedNamespace}$${expectedTypeName} `
+        + `type ${expectedTypeId}, got ${typeMeta.getNs()}$${typeMeta.getTypeName()} `
+        + `type ${typeMeta.getTypeId()}`,
+      );
+    }
+  }
+
+  private readTypeMetaFromHeader(
+    dynamicTypeId: number,
+    headerLow: number,
+    headerHigh: number,
+    headerHash: number,
+    localHash?: number,
+    original?: Serializer,
+  ): TypeMeta {
+    // Header-cache hits intentionally compare only TypeMeta.headerHash, then skip
+    // the body. That hash is the protocol metadata identity and is set only after
+    // body/hash validation. Do not add low-bit state, parallel header slots,
+    // rehashing, limits, exact-local checks, allocation, or policy work here;
+    // the miss path owns that.
+    const cachedTypeMeta = this.findCachedTypeMeta(headerHash);
+    if (cachedTypeMeta !== undefined) {
+      TypeMeta.skipBodyByHeaderLow(this.reader, headerLow);
+      this.typeMeta[dynamicTypeId] = cachedTypeMeta;
+      return cachedTypeMeta;
+    }
+
+    const cached = this.typeMetaCache.get(headerHash);
+    let typeMeta: TypeMeta;
+    if (cached !== undefined && cached.headerHash === headerHash) {
+      TypeMeta.skipBodyByHeaderLow(this.reader, headerLow);
+      typeMeta = cached;
+      this.rememberTypeMeta(typeMeta);
+    } else {
+      const typeMetaStart = this.reader.readGetCursor() - 8;
+      const header = (BigInt(headerHigh) << 32n) | BigInt(headerLow);
+      typeMeta = TypeMeta.fromBytesAfterHeader(
+        this.reader,
+        header,
+        this.typeResolver.config.maxTypeFields,
+        this.typeResolver.config.maxTypeMetaBytes,
+      );
+      const typeMetaEnd = this.reader.readGetCursor();
       if (
-        this.compatibleReadSerializers.size
-        < ReadContext.MAX_CACHED_COMPATIBLE_READ_SERIALIZER
+        this.matchesExactLocalTypeMeta(typeMeta, typeMetaStart, typeMetaEnd)
       ) {
-        this.compatibleReadSerializers.set(remoteHash, {
-          localHash: expectedHash,
-          serializer,
-        });
+        this.cacheTypeMeta(headerHash, typeMeta, undefined);
+      } else {
+        const localSerializer = original ?? this.serializerByTypeMeta(typeMeta);
+        if (
+          localSerializer === undefined
+          && !TypeId.structType(typeMeta.getTypeId())
+        ) {
+          throw new Error(
+            `can't find serializer for TypeMeta ${typeMeta.getNs()}$${typeMeta.getTypeName()}`,
+          );
+        }
+        const typeKey = this.checkRemoteTypeMetaLimit(typeMeta);
+        if (
+          localSerializer !== undefined
+          && TypeId.structType(typeMeta.getTypeId())
+        ) {
+          const expectedHash = localHash ?? localSerializer.getHash();
+          if (expectedHash !== typeMeta.getHash()) {
+            this.ensureCompatibleReadSerializer(
+              typeMeta,
+              expectedHash,
+              typeMeta.getHash(),
+              localSerializer,
+            );
+          }
+        } else if (
+          localHash !== undefined
+          && localHash !== typeMeta.getHash()
+        ) {
+          this.ensureCompatibleReadSerializer(
+            typeMeta,
+            localHash,
+            typeMeta.getHash(),
+            original,
+          );
+        }
+        this.cacheTypeMeta(headerHash, typeMeta, typeKey);
       }
-      return serializer;
     }
-    return undefined;
+    this.typeMeta[dynamicTypeId] = typeMeta;
+    return typeMeta;
+  }
+
+  private ensureCompatibleReadSerializer(
+    typeMeta: TypeMeta,
+    localHash: number,
+    remoteHash: number,
+    original?: Serializer,
+  ): Serializer {
+    const cached = this.compatibleReadSerializers.get(remoteHash);
+    if (cached !== undefined && cached.localHash === localHash) {
+      return cached.serializer;
+    }
+    const serializer = this.genSerializerByTypeMetaRuntime(typeMeta, original);
+    this.compatibleReadSerializers.set(remoteHash, {
+      localHash,
+      serializer,
+    });
+    return serializer;
+  }
+
+  private checkRemoteTypeMetaLimit(typeMeta: TypeMeta) {
+    const typeKey = TypeId.isNamedType(typeMeta.getTypeId())
+      ? `${typeMeta.getNs()}\u0000${typeMeta.getTypeName()}`
+      : typeMeta.getUserTypeId();
+    const versionsByType = this.remoteSchemaVersionsByType;
+    const versionsForType = versionsByType?.get(typeKey) ?? 0;
+    const maxSchemaVersionsPerType
+      = this.typeResolver.config.maxSchemaVersionsPerType;
+    if (versionsForType >= maxSchemaVersionsPerType) {
+      throw new Error(
+        `Remote schema version limit exceeded for type ${String(typeKey)}: `
+        + `${versionsForType} >= ${maxSchemaVersionsPerType}. The data may `
+        + "be malicious. If the data is not malicious, please increase "
+        + "maxSchemaVersionsPerType.",
+      );
+    }
+    const acceptedTypeCount
+      = versionsForType === 0
+        ? (versionsByType?.size ?? 0) + 1
+        : versionsByType!.size;
+    const maxAverageSchemaVersionsPerType
+      = this.typeResolver.config.maxAverageSchemaVersionsPerType;
+    const globalLimit = Math.max(
+      ReadContext.MIN_REMOTE_TYPE_META_LIMIT,
+      acceptedTypeCount * maxAverageSchemaVersionsPerType,
+    );
+    if (this.totalAcceptedSchemaVersions >= globalLimit) {
+      throw new Error(
+        `Remote schema version limit exceeded: ${this.totalAcceptedSchemaVersions} `
+        + `metadata versions for ${acceptedTypeCount} accepted remote types `
+        + `exceeds the average limit ${maxAverageSchemaVersionsPerType}. `
+        + "The data may be malicious. If the data is not malicious, please "
+        + "increase maxAverageSchemaVersionsPerType.",
+      );
+    }
+    return typeKey;
+  }
+
+  private cacheTypeMeta(
+    headerHash: number,
+    typeMeta: TypeMeta,
+    typeKey: string | number | undefined,
+  ) {
+    this.typeMetaCache.set(headerHash, typeMeta);
+    this.rememberTypeMeta(typeMeta);
+    if (typeKey !== undefined) {
+      let versionsByType = this.remoteSchemaVersionsByType;
+      if (versionsByType === undefined) {
+        versionsByType = new Map();
+        this.remoteSchemaVersionsByType = versionsByType;
+      }
+      const versionsForType = versionsByType.get(typeKey) ?? 0;
+      versionsByType.set(typeKey, versionsForType + 1);
+      this.totalAcceptedSchemaVersions++;
+    }
+  }
+
+  private serializerByTypeMeta(typeMeta: TypeMeta) {
+    const typeId = typeMeta.getTypeId();
+    if (TypeId.isNamedType(typeId)) {
+      return this.typeResolver.getSerializerByName(
+        `${typeMeta.getNs()}$${typeMeta.getTypeName()}`,
+      );
+    }
+    if (TypeId.needsUserTypeId(typeId)) {
+      return this.typeResolver.getSerializerById(
+        typeId,
+        typeMeta.getUserTypeId(),
+      );
+    }
+    return this.typeResolver.getSerializerById(typeId);
+  }
+
+  private matchesExactLocalTypeMeta(
+    remoteTypeMeta: TypeMeta,
+    start: number,
+    end: number,
+  ): boolean {
+    const serializer = this.serializerByTypeMeta(remoteTypeMeta);
+    const localBytes = serializer?.getTypeMetaBytes?.();
+    if (localBytes === undefined) {
+      return false;
+    }
+    if (end - start !== localBytes.length) {
+      return false;
+    }
+    const remoteBytes = this.reader.bufferRefAt(start, localBytes.length);
+    for (let i = 0; i < localBytes.length; i++) {
+      if (remoteBytes[i] !== localBytes[i]) {
+        return false;
+      }
+    }
+    return true;
   }
 
   private canonicalTypeId(typeId: number): number {
diff --git a/javascript/packages/core/lib/fory.ts b/javascript/packages/core/lib/fory.ts
index 3c122d790..05b424b1c 100644
--- a/javascript/packages/core/lib/fory.ts
+++ b/javascript/packages/core/lib/fory.ts
@@ -34,6 +34,10 @@ import { ReadContext, WriteContext } from "./context";
 
 const DEFAULT_DEPTH_LIMIT = 50 as const;
 const MIN_DEPTH_LIMIT = 2 as const;
+const DEFAULT_MAX_TYPE_FIELDS = 512 as const;
+const DEFAULT_MAX_TYPE_META_BYTES = 4096 as const;
+const DEFAULT_MAX_SCHEMA_VERSIONS_PER_TYPE = 10 as const;
+const DEFAULT_MAX_AVERAGE_SCHEMA_VERSIONS_PER_TYPE = 3 as const;
 export default class Fory {
   readonly typeResolver: TypeResolver;
   readonly anySerializer: Serializer;
@@ -67,10 +71,48 @@ export default class Fory {
   }
 
   private initConfig(config: Partial<Config> | undefined) {
+    const maxTypeFields = config?.maxTypeFields ?? DEFAULT_MAX_TYPE_FIELDS;
+    if (!Number.isInteger(maxTypeFields) || maxTypeFields <= 0) {
+      throw new Error(
+        `maxTypeFields must be a positive integer but got ${maxTypeFields}`,
+      );
+    }
+    const maxTypeMetaBytes
+      = config?.maxTypeMetaBytes ?? DEFAULT_MAX_TYPE_META_BYTES;
+    if (!Number.isInteger(maxTypeMetaBytes) || maxTypeMetaBytes <= 0) {
+      throw new Error(
+        `maxTypeMetaBytes must be a positive integer but got ${maxTypeMetaBytes}`,
+      );
+    }
+    const maxSchemaVersionsPerType
+      = config?.maxSchemaVersionsPerType ?? DEFAULT_MAX_SCHEMA_VERSIONS_PER_TYPE;
+    if (
+      !Number.isInteger(maxSchemaVersionsPerType)
+      || maxSchemaVersionsPerType <= 0
+    ) {
+      throw new Error(
+        `maxSchemaVersionsPerType must be a positive integer but got ${maxSchemaVersionsPerType}`,
+      );
+    }
+    const maxAverageSchemaVersionsPerType
+      = config?.maxAverageSchemaVersionsPerType
+      ?? DEFAULT_MAX_AVERAGE_SCHEMA_VERSIONS_PER_TYPE;
+    if (
+      !Number.isInteger(maxAverageSchemaVersionsPerType)
+      || maxAverageSchemaVersionsPerType <= 0
+    ) {
+      throw new Error(
+        `maxAverageSchemaVersionsPerType must be a positive integer but got ${maxAverageSchemaVersionsPerType}`,
+      );
+    }
     return {
       ref: Boolean(config?.ref),
       useSliceString: Boolean(config?.useSliceString),
       maxDepth: config?.maxDepth,
+      maxTypeFields,
+      maxTypeMetaBytes,
+      maxSchemaVersionsPerType,
+      maxAverageSchemaVersionsPerType,
       hooks: config?.hooks || {},
       compatible: config?.compatible ?? true,
       hps: config?.hps,
diff --git a/javascript/packages/core/lib/gen/builder.ts b/javascript/packages/core/lib/gen/builder.ts
index fe60dc128..6b88f22e1 100644
--- a/javascript/packages/core/lib/gen/builder.ts
+++ b/javascript/packages/core/lib/gen/builder.ts
@@ -375,11 +375,17 @@ class TypeMetaContextBuilder {
     return `${this.readHolder}.readTypeMeta()`;
   }
 
-  readTypeMetaIfSchemaChanged(expectedHash: string, original?: string) {
+  readNamedTypeMeta(typeId: number, namespace: string, typeName: string) {
+    const safeNamespace = CodecBuilder.replaceBackslashAndQuote(namespace);
+    const safeTypeName = CodecBuilder.replaceBackslashAndQuote(typeName);
+    return `${this.readHolder}.readNamedTypeMeta(${typeId}, "${safeNamespace}", "${safeTypeName}")`;
+  }
+
+  readCompatibleStructSerializer(localHash: string, original?: string) {
     if (original) {
-      return `${this.readHolder}.readTypeMetaIfSchemaChanged(${expectedHash}, ${original})`;
+      return `${this.readHolder}.readCompatibleStructSerializer(${localHash}, ${original})`;
     }
-    return `${this.readHolder}.readTypeMetaIfSchemaChanged(${expectedHash})`;
+    return `${this.readHolder}.readCompatibleStructSerializer(${localHash})`;
   }
 
   genSerializerByTypeMetaRuntime(typeMeta: string, original?: string) {
diff --git a/javascript/packages/core/lib/gen/enum.ts b/javascript/packages/core/lib/gen/enum.ts
index 22c70208b..b0f0cb320 100644
--- a/javascript/packages/core/lib/gen/enum.ts
+++ b/javascript/packages/core/lib/gen/enum.ts
@@ -23,6 +23,7 @@ import { BaseSerializerGenerator } from "./serializer";
 import { CodegenRegistry } from "./router";
 import { TypeId, MaxUInt32 } from "../type";
 import { Scope } from "./scope";
+import { TypeMeta } from "../meta/TypeMeta";
 
 class EnumSerializerGenerator extends BaseSerializerGenerator {
   typeInfo: TypeInfo;
@@ -99,17 +100,27 @@ class EnumSerializerGenerator extends BaseSerializerGenerator {
         readUserTypeIdStmt = `${this.builder.reader.readVarUint32Small7()};`;
         break;
       case TypeId.NAMED_ENUM:
-        namesStmt = `
-          ${
-            // skip the namespace
-            this.builder.metaStringResolver.readNamespace()
-          }
+        if (this.builder.resolver.isCompatible()) {
+          namesStmt = `
+            ${this.builder.typeMetaResolver.readNamedTypeMeta(
+              TypeId.NAMED_ENUM,
+              this.typeInfo.namespace,
+              this.typeInfo.typeName,
+            )};
+          `;
+        } else {
+          namesStmt = `
+            ${
+              // skip the namespace
+              this.builder.metaStringResolver.readNamespace()
+            }
 
-          ${
-          // skip the namespace
-          this.builder.metaStringResolver.readTypeName()
-          }
-        `;
+            ${
+            // skip the type name
+            this.builder.metaStringResolver.readTypeName()
+            }
+          `;
+        }
         break;
       default:
         break;
@@ -133,7 +144,10 @@ class EnumSerializerGenerator extends BaseSerializerGenerator {
         writeUserTypeIdStmt = this.builder.writer.writeVarUint32Small7(this.typeInfo.userTypeId);
         break;
       case TypeId.NAMED_ENUM:
-        {
+        if (this.builder.resolver.isCompatible()) {
+          const bytes = this.scope.declare("enumTypeInfoBytes", `new Uint8Array([${TypeMeta.fromTypeInfo(this.typeInfo, this.builder.resolver).toBytes().join(",")}])`);
+          typeMeta = this.builder.typeMetaResolver.writeTypeMeta(this.builder.getTypeInfo(), bytes);
+        } else {
           const typeInfo = this.typeInfo;
           const nsBytes = this.scope.declare("nsBytes", this.builder.metaStringResolver.encodeNamespace(CodecBuilder.replaceBackslashAndQuote(typeInfo.namespace)));
           const typeNameBytes = this.scope.declare("typeNameBytes", this.builder.metaStringResolver.encodeTypeName(CodecBuilder.replaceBackslashAndQuote(typeInfo.typeName)));
diff --git a/javascript/packages/core/lib/gen/ext.ts b/javascript/packages/core/lib/gen/ext.ts
index 325f2c18c..704495edf 100644
--- a/javascript/packages/core/lib/gen/ext.ts
+++ b/javascript/packages/core/lib/gen/ext.ts
@@ -79,7 +79,6 @@ class ExtSerializerGenerator extends BaseSerializerGenerator {
   }
 
   readTypeInfo(): string {
-    const typeMeta = this.scope.uniqueName("typeMeta");
     const internalTypeId = this.getInternalTypeId();
     let namesStmt = "";
     let typeMetaStmt = "";
@@ -100,7 +99,11 @@ class ExtSerializerGenerator extends BaseSerializerGenerator {
           `;
         } else {
           typeMetaStmt = `
-          const ${typeMeta} = ${this.builder.typeMetaResolver.readTypeMeta()};
+          ${this.builder.typeMetaResolver.readNamedTypeMeta(
+            TypeId.NAMED_EXT,
+            this.typeInfo.namespace,
+            this.typeInfo.typeName,
+          )};
           `;
         }
         break;
diff --git a/javascript/packages/core/lib/gen/serializer.ts b/javascript/packages/core/lib/gen/serializer.ts
index ac8506a47..439e1f490 100644
--- a/javascript/packages/core/lib/gen/serializer.ts
+++ b/javascript/packages/core/lib/gen/serializer.ts
@@ -52,6 +52,7 @@ export interface SerializerGenerator {
   read(assignStmt: (v: string) => string, refState: string): string;
   readEmbed(): any;
   getHash(): string;
+  getTypeMetaBytes(): string;
 
   getType(): number;
   getTypeId(): number | undefined;
@@ -275,6 +276,10 @@ export abstract class BaseSerializerGenerator implements SerializerGenerator {
     return "0";
   }
 
+  getTypeMetaBytes(): string {
+    return "undefined";
+  }
+
   toSerializer() {
     this.scope.assertNameNotDuplicate("read");
     this.scope.assertNameNotDuplicate("readInner");
@@ -289,6 +294,9 @@ export abstract class BaseSerializerGenerator implements SerializerGenerator {
       const getHash = () => {
         return ${this.getHash()};
       }
+      const getTypeMetaBytes = () => {
+        return ${this.getTypeMetaBytes()};
+      }
       const write = (v) => {
         ${this.write("v")}
       };
@@ -332,6 +340,7 @@ export abstract class BaseSerializerGenerator implements SerializerGenerator {
               getUserTypeId: () => ${this.getUserTypeId()},
               getTypeInfo: () => typeInfo,
               getHash,
+              getTypeMetaBytes,
 
               write,
               writeRef,
diff --git a/javascript/packages/core/lib/gen/struct.ts b/javascript/packages/core/lib/gen/struct.ts
index 3c791e705..5b02a53a1 100644
--- a/javascript/packages/core/lib/gen/struct.ts
+++ b/javascript/packages/core/lib/gen/struct.ts
@@ -1085,7 +1085,7 @@ class StructSerializerGenerator extends BaseSerializerGenerator {
           }`;
       }
       return `
-          const ${changedSerializer} = ${this.builder.typeMetaResolver.readTypeMetaIfSchemaChanged(localHash)};
+          const ${changedSerializer} = ${this.builder.typeMetaResolver.readCompatibleStructSerializer(localHash)};
           if (${changedSerializer} !== undefined) {
             ${onMetaChanged?.(changedSerializer) ?? `return ${changedSerializer};`}
           }${unchangedBranch}
@@ -1166,7 +1166,7 @@ class StructSerializerGenerator extends BaseSerializerGenerator {
       const hoistedHash = scope.declare("serHash", `${hoisted}.getHash()`);
       return `
               ${builder.reader.readUint8()};
-              const ${changedSerializer} = ${builder.typeMetaResolver.readTypeMetaIfSchemaChanged(hoistedHash, hoisted)};
+              const ${changedSerializer} = ${builder.typeMetaResolver.readCompatibleStructSerializer(hoistedHash, hoisted)};
               if (${changedSerializer} !== undefined) {
                 ${onMetaChanged(changedSerializer)}
               } else {
@@ -1283,7 +1283,7 @@ class StructSerializerGenerator extends BaseSerializerGenerator {
       case TypeId.NAMED_COMPATIBLE_STRUCT:
       case TypeId.COMPATIBLE_STRUCT:
         {
-          const bytes = this.scope.declare("typeInfoBytes", `new Uint8Array([${TypeMeta.fromTypeInfo(this.typeInfo, this.builder.resolver).toBytes().join(",")}])`);
+          const bytes = this.typeMetaBytesExpr();
           typeMeta = this.builder.typeMetaResolver.writeTypeMeta(this.builder.getTypeInfo(), bytes);
         }
         break;
@@ -1298,7 +1298,7 @@ class StructSerializerGenerator extends BaseSerializerGenerator {
               ${this.builder.metaStringResolver.writeBytes(typeNameBytes)}
             `;
           } else {
-            const bytes = this.scope.declare("typeInfoBytes", `new Uint8Array([${TypeMeta.fromTypeInfo(this.typeInfo, this.builder.resolver).toBytes().join(",")}])`);
+            const bytes = this.typeMetaBytesExpr();
             typeMeta = this.builder.typeMetaResolver.writeTypeMeta(this.builder.getTypeInfo(), bytes);
           }
         }
@@ -1331,6 +1331,17 @@ class StructSerializerGenerator extends BaseSerializerGenerator {
   getHash(): string {
     return TypeMeta.fromTypeInfo(this.typeInfo, this.builder.resolver).getHash().toString();
   }
+
+  getTypeMetaBytes(): string {
+    return this.typeMetaBytesExpr();
+  }
+
+  private typeMetaBytesExpr(): string {
+    return this.scope.declare(
+      "typeInfoBytes",
+      `new Uint8Array([${this.typeMeta.toBytes().join(",")}])`,
+    );
+  }
 }
 
 CodegenRegistry.register(TypeId.STRUCT, StructSerializerGenerator);
diff --git a/javascript/packages/core/lib/gen/union.ts b/javascript/packages/core/lib/gen/union.ts
index 2370519a1..c95bf1008 100644
--- a/javascript/packages/core/lib/gen/union.ts
+++ b/javascript/packages/core/lib/gen/union.ts
@@ -245,9 +245,12 @@ class UnionSerializerGenerator extends BaseSerializerGenerator {
         break;
       case TypeId.NAMED_UNION:
         if (this.builder.resolver.isCompatible()) {
-          const typeMeta = this.scope.uniqueName("unionTypeMeta");
           namesStmt = `
-            const ${typeMeta} = ${this.builder.typeMetaResolver.readTypeMeta()};
+            ${this.builder.typeMetaResolver.readNamedTypeMeta(
+              TypeId.NAMED_UNION,
+              this.typeInfo.namespace,
+              this.typeInfo.typeName,
+            )};
           `;
         } else {
           namesStmt = `
diff --git a/javascript/packages/core/lib/meta/TypeMeta.ts b/javascript/packages/core/lib/meta/TypeMeta.ts
index 90c4696c4..00d4785c9 100644
--- a/javascript/packages/core/lib/meta/TypeMeta.ts
+++ b/javascript/packages/core/lib/meta/TypeMeta.ts
@@ -45,6 +45,8 @@ const UINT64_MASK = 0xffffffffffffffffn;
 const HEADER_HASH_MASK = UINT64_MASK ^ ((1n << HASH_SHIFT_BITS) - 1n);
 const BIG_NAME_THRESHOLD = 0b111111;
 const MAX_TYPE_META_NESTING = 128;
+const DEFAULT_MAX_TYPE_FIELDS = 512;
+const DEFAULT_MAX_TYPE_META_BYTES = 4096;
 
 const PRIMITIVE_TYPE_IDS = [
   TypeId.BOOL,
@@ -286,7 +288,10 @@ function nonStructTypeId(kindCode: number): number {
   }
 }
 export class TypeMeta {
-  private headerHash: number | null;
+  // The 52-bit TypeMeta header hash is the checked metadata identity. It is
+  // precise in JS Number and already includes the low header bits as hash input;
+  // hot cache hits must compare this value directly without extra low-bit state.
+  headerHash: number | null;
   private readonly compressed: boolean;
   private bytes: Uint8Array | null = null;
 
@@ -448,6 +453,7 @@ export class TypeMeta {
   /**
    * Skip the type meta body after the caller has matched the full validated
    * header key. Only the low header bits are needed to decode the body-size field.
+   * Cache-hit skips must not allocate or materialize the opaque metadata body.
    */
   static skipBodyByHeaderLow(reader: BinaryReader, headerLow: number) {
     const metaSize = TypeMeta.readMetaSizeFromLow(reader, headerLow);
@@ -462,13 +468,21 @@ export class TypeMeta {
    * Parse the type meta body after the header has already been consumed
    * by readHeader(). Used by ReadContext to avoid re-reading the header.
    */
-  static fromBytesAfterHeader(reader: BinaryReader, header: bigint): TypeMeta {
+  static fromBytesAfterHeader(
+    reader: BinaryReader,
+    header: bigint,
+    maxTypeFields = DEFAULT_MAX_TYPE_FIELDS,
+    maxTypeMetaBytes = DEFAULT_MAX_TYPE_META_BYTES,
+  ): TypeMeta {
     TypeMeta.validateGlobalHeader(header);
     const metaSize = TypeMeta.readMetaSize(reader, header);
+    TypeMeta.checkTypeMetaBytes(metaSize, maxTypeMetaBytes);
     const compressed = false;
     const headerHash = Number(header >> HASH_SHIFT_BITS);
 
     const bodyStart = reader.readGetCursor();
+    // Size limits are not byte-availability proof. Keep this readable-byte
+    // check before parsing, slicing, copying, or caching data from metaSize.
     reader.checkReadableBytes(metaSize);
     const bodyEnd = bodyStart + metaSize;
     const classHeader = reader.readUint8();
@@ -495,6 +509,7 @@ export class TypeMeta {
       if (numFields === SMALL_NUM_FIELDS_THRESHOLD) {
         numFields += reader.readVarUInt32();
       }
+      TypeMeta.checkTypeFields(numFields, maxTypeFields);
     } else {
       if ((classHeader & 0b01110000) !== 0) {
         throw new Error("invalid TypeMeta kind header");
@@ -572,6 +587,27 @@ export class TypeMeta {
     return metaSize;
   }
 
+  private static checkTypeMetaBytes(
+    metaSize: number,
+    maxTypeMetaBytes: number,
+  ) {
+    if (metaSize > maxTypeMetaBytes) {
+      throw new Error(
+        `Type metadata body size ${metaSize} exceeds maxTypeMetaBytes ${maxTypeMetaBytes}. `
+        + "The data may be malicious. If the data is not malicious, please increase maxTypeMetaBytes.",
+      );
+    }
+  }
+
+  private static checkTypeFields(numFields: number, maxTypeFields: number) {
+    if (numFields > maxTypeFields) {
+      throw new Error(
+        `Type metadata field count ${numFields} exceeds maxTypeFields ${maxTypeFields}. `
+        + "The data may be malicious. If the data is not malicious, please increase maxTypeFields.",
+      );
+    }
+  }
+
   private static validateParsedBodyHash(header: bigint, body: Uint8Array) {
     const expectedHeaderHash = TypeMeta.headerHashBits(
       body,
diff --git a/javascript/packages/core/lib/type.ts b/javascript/packages/core/lib/type.ts
index 5999c99b7..bfb4a942f 100644
--- a/javascript/packages/core/lib/type.ts
+++ b/javascript/packages/core/lib/type.ts
@@ -241,6 +241,7 @@ export type Serializer<T = any> = {
   getTypeId: () => number;
   getUserTypeId: () => number;
   getHash: () => number;
+  getTypeMetaBytes?: () => Uint8Array | undefined;
 
   // for writing
   write: (v: T) => void;
@@ -291,6 +292,10 @@ export interface Config {
   ref: boolean;
   useSliceString: boolean;
   maxDepth?: number;
+  maxTypeFields: number;
+  maxTypeMetaBytes: number;
+  maxSchemaVersionsPerType: number;
+  maxAverageSchemaVersionsPerType: number;
   hooks: {
     afterCodeGenerated?: (code: string) => string;
   };
diff --git a/javascript/packages/core/package.json b/javascript/packages/core/package.json
index 71112f067..49b28b3d6 100644
--- a/javascript/packages/core/package.json
+++ b/javascript/packages/core/package.json
@@ -6,6 +6,7 @@
   "main": "dist/index.js",
   "scripts": {
     "build": "tsc",
+    "test": "npm run build && node --test test/*.test.js",
     "prepublishOnly": "npm run build"
   },
   "files": [
diff --git a/javascript/packages/core/test/schema-limit.test.js b/javascript/packages/core/test/schema-limit.test.js
new file mode 100644
index 000000000..bb0a5ceef
--- /dev/null
+++ b/javascript/packages/core/test/schema-limit.test.js
@@ -0,0 +1,554 @@
+/*
+ * 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.
+ */
+
+"use strict";
+
+const assert = require("node:assert/strict");
+const runTest =
+  typeof globalThis.expect === "function" && typeof globalThis.test === "function"
+    ? globalThis.test
+    : require("node:test").test;
+const { ReadContext } = require("../dist/lib/context");
+const { AnyHelper } = require("../dist/lib/gen/any");
+const { FieldInfo, TypeMeta } = require("../dist/lib/meta/TypeMeta");
+const { TypeId } = require("../dist/lib/type");
+const { Type } = require("../dist/lib/typeInfo");
+
+function context(typeResolver = {}, config = {}) {
+  const fullConfig = {
+    compatible: true,
+    maxTypeFields: 512,
+    maxTypeMetaBytes: 4096,
+    maxAverageSchemaVersionsPerType: 3,
+    maxSchemaVersionsPerType: 1,
+    useSliceString: false,
+    ...config,
+  };
+  const resolver = {
+    config: fullConfig,
+    getSerializerById() {
+      return undefined;
+    },
+    getSerializerByName() {
+      return undefined;
+    },
+    ...typeResolver,
+  };
+  return new ReadContext(resolver, fullConfig);
+}
+
+function remoteStruct(
+  name,
+  fieldName,
+  fieldType = Type.int32({ encoding: "fixed" }),
+  typeId = TypeId.NAMED_STRUCT,
+  userTypeId = -1,
+) {
+  return new TypeMeta([new FieldInfo(
+    fieldName,
+    fieldType.typeId,
+    fieldType.userTypeId,
+    fieldType.trackingRef === true,
+    fieldType.nullable === true,
+    fieldType.options,
+  )], {
+    namespace: "example",
+    typeId,
+    typeName: name,
+    userTypeId,
+  });
+}
+
+function anyStruct(fieldName, fieldType = Type.int32({ encoding: "fixed" })) {
+  return remoteStruct("", fieldName, fieldType, TypeId.COMPATIBLE_STRUCT, 901);
+}
+
+function remoteNamedNonStruct(name, typeId) {
+  return new TypeMeta([], {
+    namespace: "example",
+    typeId,
+    typeName: name,
+    userTypeId: -1,
+  });
+}
+
+function readTypeMeta(readContext, typeMeta) {
+  const encoded = typeMeta.toBytes();
+  const bytes = new Uint8Array(encoded.length + 1);
+  bytes[0] = 0;
+  bytes.set(encoded, 1);
+  readContext.reset(bytes);
+  return readContext.readTypeMeta();
+}
+
+function readNamedTypeMeta(readContext, typeId, namespace, typeName, typeMeta) {
+  const encoded = typeMeta.toBytes();
+  const bytes = new Uint8Array(encoded.length + 1);
+  bytes[0] = 0;
+  bytes.set(encoded, 1);
+  readContext.reset(bytes);
+  return readContext.readNamedTypeMeta(typeId, namespace, typeName);
+}
+
+function headerParts(typeMeta) {
+  const encoded = typeMeta.toBytes();
+  const view = new DataView(encoded.buffer, encoded.byteOffset, encoded.byteLength);
+  const header = view.getBigUint64(0, true);
+  return {
+    low: Number(header & 0xffffffffn),
+    high: Number(header >> 32n),
+  };
+}
+
+function readCompatibleStructSerializer(readContext, expectedHash, original, typeMeta) {
+  const encoded = typeMeta.toBytes();
+  const bytes = new Uint8Array(encoded.length + 1);
+  bytes[0] = 0;
+  bytes.set(encoded, 1);
+  readContext.reset(bytes);
+  return readContext.readCompatibleStructSerializer(expectedHash, original);
+}
+
+function detectAnySerializer(readContext, typeMeta) {
+  const encoded = typeMeta.toBytes();
+  const bytes = new Uint8Array(encoded.length + 2);
+  bytes[0] = TypeId.COMPATIBLE_STRUCT;
+  bytes[1] = 0;
+  bytes.set(encoded, 2);
+  readContext.reset(bytes);
+  return AnyHelper.detectSerializer(readContext);
+}
+
+function localSerializer(typeInfo) {
+  const typeMeta = TypeMeta.fromTypeInfo(typeInfo);
+  return {
+    getHash() {
+      return typeMeta.getHash();
+    },
+    getTypeInfo() {
+      return typeInfo;
+    },
+    getTypeMetaBytes() {
+      return typeMeta.toBytes();
+    },
+  };
+}
+
+runTest("remote schema limit rejects extra versions", () => {
+  const readContext = context();
+  readTypeMeta(readContext, remoteStruct("Shared", "first"));
+  assert.throws(
+    () => readTypeMeta(readContext, remoteStruct("Shared", "second")),
+    /maxSchemaVersionsPerType/,
+  );
+});
+
+runTest("remote non-struct TypeMeta uses schema limit", () => {
+  const readContext = context({
+    getSerializerByName(name) {
+      return name === "example$SharedEnum" ? {} : undefined;
+    },
+  });
+  readTypeMeta(readContext, remoteNamedNonStruct("SharedEnum", TypeId.NAMED_ENUM));
+  assert.throws(
+    () => readTypeMeta(readContext, remoteNamedNonStruct("SharedEnum", TypeId.NAMED_EXT)),
+    /maxSchemaVersionsPerType/,
+  );
+});
+
+runTest("failed non-struct TypeMeta does not consume schema limit", () => {
+  let registered = false;
+  const readContext = context({
+    getSerializerByName(name) {
+      return registered && name === "example$SharedEnum" ? {} : undefined;
+    },
+  });
+
+  assert.throws(
+    () => readTypeMeta(readContext, remoteNamedNonStruct("SharedEnum", TypeId.NAMED_ENUM)),
+    /can't find serializer/,
+  );
+
+  registered = true;
+  assert.doesNotThrow(
+    () => readTypeMeta(readContext, remoteNamedNonStruct("SharedEnum", TypeId.NAMED_EXT)),
+  );
+});
+
+runTest("exact local non-struct TypeMeta bypasses schema limit", () => {
+  const enumInfo = Type.enum({ namespace: "example", typeName: "SharedEnum" }, { A: 0 });
+  const enumMeta = TypeMeta.fromTypeInfo(enumInfo);
+  const localSerializer = {
+    getTypeInfo() {
+      return enumInfo;
+    },
+    getTypeMetaBytes() {
+      return enumMeta.toBytes();
+    },
+  };
+  const readContext = context({
+    computeTypeId(typeInfo) {
+      return typeInfo.typeId;
+    },
+    getSerializerByName(name) {
+      return name === "example$SharedEnum" ? localSerializer : undefined;
+    },
+  });
+
+  readNamedTypeMeta(readContext, TypeId.NAMED_ENUM, "example", "SharedEnum", enumMeta);
+  assert.doesNotThrow(() => readNamedTypeMeta(
+    readContext,
+    TypeId.NAMED_EXT,
+    "example",
+    "SharedEnum",
+    remoteNamedNonStruct("SharedEnum", TypeId.NAMED_EXT),
+  ));
+
+  const genericReadContext = context({
+    computeTypeId(typeInfo) {
+      return typeInfo.typeId;
+    },
+    getSerializerByName(name) {
+      return name === "example$SharedEnum" ? localSerializer : undefined;
+    },
+  });
+  readTypeMeta(genericReadContext, enumMeta);
+  assert.doesNotThrow(() => readTypeMeta(
+    genericReadContext,
+    remoteNamedNonStruct("SharedEnum", TypeId.NAMED_EXT),
+  ));
+});
+
+runTest("named enum TypeMeta validates declared owner before caching", () => {
+  const colorInfo = Type.enum({ namespace: "example", typeName: "Color" }, { Red: 0 });
+  const otherInfo = Type.enum({ namespace: "example", typeName: "Other" }, { Blue: 0 });
+  const colorMeta = TypeMeta.fromTypeInfo(colorInfo);
+  const otherMeta = TypeMeta.fromTypeInfo(otherInfo);
+  const readContext = context({
+    getSerializerByName(name) {
+      if (name === "example$Color") {
+        return localSerializer(colorInfo);
+      }
+      if (name === "example$Other") {
+        return localSerializer(otherInfo);
+      }
+      return undefined;
+    },
+  });
+
+  assert.throws(
+    () => readNamedTypeMeta(
+      readContext,
+      TypeId.NAMED_ENUM,
+      "example",
+      "Color",
+      otherMeta,
+    ),
+    /TypeMeta mismatch/,
+  );
+
+  const wrongHeader = headerParts(otherMeta);
+  assert.equal(
+    readContext.typeMetaCache.get(wrongHeader.high)?.get(wrongHeader.low),
+    undefined,
+  );
+  assert.doesNotThrow(
+    () => readNamedTypeMeta(
+      readContext,
+      TypeId.NAMED_ENUM,
+      "example",
+      "Color",
+      colorMeta,
+    ),
+  );
+});
+
+runTest("TypeMeta field limit rejects large struct metadata", () => {
+  const readContext = context({}, { maxTypeFields: 1 });
+  const fieldType = Type.int32({ encoding: "fixed" });
+  const typeMeta = new TypeMeta([
+    new FieldInfo("first", fieldType.typeId, fieldType.userTypeId, false, false, fieldType.options),
+    new FieldInfo("second", fieldType.typeId, fieldType.userTypeId, false, false, fieldType.options),
+  ], {
+    namespace: "example",
+    typeId: TypeId.NAMED_STRUCT,
+    typeName: "TooManyFields",
+    userTypeId: -1,
+  });
+
+  assert.throws(() => readTypeMeta(readContext, typeMeta), /maxTypeFields/);
+});
+
+runTest("TypeMeta body limit rejects large metadata", () => {
+  const readContext = context({}, { maxTypeMetaBytes: 1 });
+
+  assert.throws(
+    () => readTypeMeta(readContext, remoteStruct("LargeMeta", "value")),
+    /maxTypeMetaBytes/,
+  );
+});
+
+runTest("TypeMeta cache hit skips current body", () => {
+  const readContext = context();
+  const typeMeta = remoteStruct("Cached", "value");
+  const encoded = typeMeta.toBytes();
+
+  readTypeMeta(readContext, typeMeta);
+
+  const bytes = new Uint8Array(encoded.length + 1);
+  bytes[0] = 0;
+  bytes.set(encoded, 1);
+  bytes[bytes.length - 1] ^= 0xff;
+  const cached = readContext.readTypeMeta.bind(readContext);
+  readContext.reset(bytes);
+
+  assert.doesNotThrow(cached);
+});
+
+runTest("failed compatible TypeMeta does not consume schema limit", () => {
+  const localTypeInfo = Type.struct(
+    { namespace: "example", typeName: "Shared" },
+    { value: Type.int32({ encoding: "fixed" }) },
+  );
+  const localHash = TypeMeta.fromTypeInfo(localTypeInfo).getHash();
+  const original = localSerializer(localTypeInfo);
+  const readContext = context({
+    computeTypeId(typeInfo) {
+      return typeInfo.typeId;
+    },
+    getSerializerById() {
+      return undefined;
+    },
+    generateReadSerializer(typeInfo) {
+      return {
+        getTypeInfo() {
+          return typeInfo;
+        },
+      };
+    },
+  });
+  assert.throws(
+    () => readCompatibleStructSerializer(
+      readContext,
+      localHash,
+      original,
+      remoteStruct("Shared", "value", Type.map(Type.string(), Type.int32({ encoding: "fixed" }))),
+    ),
+    /field schema mismatch/,
+  );
+  assert.doesNotThrow(() => readCompatibleStructSerializer(
+    readContext,
+    localHash,
+    original,
+    remoteStruct("Shared", "extra"),
+  ));
+});
+
+runTest("exact local TypeMeta bypasses schema limit", () => {
+  const localTypeInfo = Type.struct(
+    { namespace: "example", typeName: "Shared" },
+    { value: Type.int32({ encoding: "fixed" }) },
+  );
+  const generatingOriginal = localSerializer(localTypeInfo);
+  const localMeta = TypeMeta.fromTypeInfo(localTypeInfo);
+  const localBytes = localMeta.toBytes();
+  const exactOriginal = {
+    getHash() {
+      return localMeta.getHash();
+    },
+    getTypeInfo() {
+      throw new Error("exact local compare must use encoded bytes");
+    },
+    getTypeMetaBytes() {
+      return localBytes;
+    },
+  };
+  let activeOriginal = generatingOriginal;
+  const localHash = generatingOriginal.getHash();
+  const readContext = context({
+    computeTypeId(typeInfo) {
+      return typeInfo.typeId;
+    },
+    getSerializerById() {
+      return undefined;
+    },
+    getSerializerByName(name) {
+      return name === "example$Shared" ? activeOriginal : undefined;
+    },
+    generateReadSerializer(typeInfo) {
+      return {
+        getTypeInfo() {
+          return typeInfo;
+        },
+      };
+    },
+  });
+
+  readCompatibleStructSerializer(
+    readContext,
+    localHash,
+    generatingOriginal,
+    remoteStruct("Shared", "extra"),
+  );
+  activeOriginal = exactOriginal;
+  assert.doesNotThrow(() => readCompatibleStructSerializer(
+    readContext,
+    localHash,
+    undefined,
+    localMeta,
+  ));
+  assert.doesNotThrow(() => readTypeMeta(
+    readContext,
+    localMeta,
+  ));
+});
+
+runTest("exact local TypeMeta does not consume schema limit", () => {
+  const localTypeInfo = Type.struct(
+    { namespace: "example", typeName: "Shared" },
+    { value: Type.int32({ encoding: "fixed" }) },
+  );
+  const original = localSerializer(localTypeInfo);
+  const readContext = context({
+    computeTypeId(typeInfo) {
+      return typeInfo.typeId;
+    },
+    getSerializerByName(name) {
+      return name === "example$Shared" ? original : undefined;
+    },
+    generateReadSerializer(typeInfo) {
+      return {
+        getTypeInfo() {
+          return typeInfo;
+        },
+      };
+    },
+  });
+
+  readTypeMeta(readContext, TypeMeta.fromTypeInfo(localTypeInfo));
+
+  assert.doesNotThrow(() => readTypeMeta(
+    readContext,
+    remoteStruct("Shared", "extra"),
+  ));
+});
+
+runTest("failed Any TypeMeta does not consume schema limit", () => {
+  const localTypeInfo = Type.struct(
+    901,
+    { value: Type.int32({ encoding: "fixed" }) },
+  );
+  const original = localSerializer(localTypeInfo);
+  const readContext = context({
+    computeTypeId(typeInfo) {
+      return typeInfo.typeId;
+    },
+    getSerializerById(typeId, userTypeId) {
+      return userTypeId === 901 ? original : undefined;
+    },
+    getSerializerByName() {
+      return undefined;
+    },
+    generateReadSerializer(typeInfo) {
+      return {
+        getHash() {
+          return TypeMeta.fromTypeInfo(typeInfo).getHash();
+        },
+        getTypeInfo() {
+          return typeInfo;
+        },
+      };
+    },
+  });
+
+  assert.throws(
+    () => detectAnySerializer(
+      readContext,
+      anyStruct("value", Type.map(Type.string(), Type.int32({ encoding: "fixed" }))),
+    ),
+    /field schema mismatch/,
+  );
+  assert.doesNotThrow(() => detectAnySerializer(readContext, anyStruct("extra")));
+});
+
+runTest("exact Any TypeMeta bypasses schema limit", () => {
+  const localTypeInfo = Type.struct(
+    901,
+    { value: Type.int32({ encoding: "fixed" }) },
+  );
+  const generatingOriginal = localSerializer(localTypeInfo);
+  const localMeta = TypeMeta.fromTypeInfo(localTypeInfo);
+  const localBytes = localMeta.toBytes();
+  const exactOriginal = {
+    getHash() {
+      return localMeta.getHash();
+    },
+    getTypeInfo() {
+      throw new Error("exact local compare must use encoded bytes");
+    },
+    getTypeMetaBytes() {
+      return localBytes;
+    },
+  };
+  let activeOriginal = generatingOriginal;
+  const readContext = context({
+    computeTypeId(typeInfo) {
+      return typeInfo.typeId;
+    },
+    getSerializerById(typeId, userTypeId) {
+      return userTypeId === 901 ? activeOriginal : undefined;
+    },
+    getSerializerByName() {
+      return undefined;
+    },
+    generateReadSerializer(typeInfo) {
+      return {
+        getHash() {
+          return TypeMeta.fromTypeInfo(typeInfo).getHash();
+        },
+        getTypeInfo() {
+          return typeInfo;
+        },
+      };
+    },
+  });
+
+  detectAnySerializer(readContext, anyStruct("extra"));
+  activeOriginal = exactOriginal;
+  assert.doesNotThrow(() => detectAnySerializer(
+    readContext,
+    localMeta,
+  ));
+  assert.doesNotThrow(() => readTypeMeta(
+    readContext,
+    localMeta,
+  ));
+});
+
+runTest("remote schema limit keeps unknown structs separate", () => {
+  const readContext = context();
+  assert.equal(
+    readTypeMeta(readContext, remoteStruct("UnknownA", "value")).getTypeName(),
+    "UnknownA",
+  );
+  assert.equal(
+    readTypeMeta(readContext, remoteStruct("UnknownB", "value")).getTypeName(),
+    "UnknownB",
+  );
+});
diff --git a/javascript/test/typemeta.test.ts b/javascript/test/typemeta.test.ts
index 83318c01e..462174064 100644
--- a/javascript/test/typemeta.test.ts
+++ b/javascript/test/typemeta.test.ts
@@ -77,6 +77,29 @@ function typeMetaRecord(typeMeta: TypeMeta): Uint8Array {
   return writer.dump();
 }
 
+function replaceFirstBytes(
+  bytes: Uint8Array,
+  search: Uint8Array,
+  replacement: Uint8Array,
+): Uint8Array {
+  expect(search.length).toBe(replacement.length);
+  const result = new Uint8Array(bytes);
+  for (let i = 0; i <= result.length - search.length; i++) {
+    let matched = true;
+    for (let j = 0; j < search.length; j++) {
+      if (result[i + j] !== search[j]) {
+        matched = false;
+        break;
+      }
+    }
+    if (matched) {
+      result.set(replacement, i);
+      return result;
+    }
+  }
+  throw new Error("bytes not found");
+}
+
 describe("typemeta", () => {
   test("splits dotted names", () => {
     const structInfo = Type.struct({ typeName: "com.example.User" }, {});
@@ -233,14 +256,150 @@ describe("typemeta", () => {
     );
   });
 
+  test("id enum does not use TypeMeta limits", () => {
+    const Color = { Green: 0, Red: 1 };
+    const fory = new Fory({
+      compatible: true,
+      maxTypeMetaBytes: 1,
... 4194 lines suppressed ...


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@fory.apache.org
For additional commands, e-mail: commits-help@fory.apache.org