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/09/21 03:58:50 UTC

(fory) branch main updated: fix(json): correct Scala codecs and Scala/Kotlin missing-field defaults (#4072)

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 509a096aa fix(json): correct Scala codecs and Scala/Kotlin missing-field defaults (#4072)
509a096aa is described below

commit 509a096aa3ac68c0ca9ee248d085db7855523584
Author: Shawn Yang <sh...@gmail.com>
AuthorDate: Mon Sep 21 11:58:45 2026 +0800

    fix(json): correct Scala codecs and Scala/Kotlin missing-field defaults (#4072)
    
    ## Why?
    
    Several documented Scala JSON values cannot be written, and the shared
    generic recursion guard rejects finite types such as
    `Box<Box<Integer>>`. Missing constructor properties also need useful
    language defaults while preserving Kotlin's non-null reference
    constraints.
    
    ## What does this PR do?
    
    - Default omitted Scala constructor parameters to `None`, numeric zero,
    `false`, fresh empty collections/arrays, or `null` as appropriate.
    Explicit constructor defaults take precedence.
    - Default omitted Kotlin non-null scalars to zero/false and nullable
    parameters to null. Explicit defaults still apply; non-null references
    without defaults, such as `val name: String`, remain required.
    - Support `ScalaTypeRef[Unit]` and nested Unit occurrences through the
    existing JSON null representation. Java `void` root writes remain
    rejected, and `Some(())` retains the documented value-or-null Option
    representation.
    - Fix populated immutable Queue, primitive mutable ArraySeq, specialized
    Tuple1/Tuple2, Duration.Undefined, and primitive generic case-class
    codegen writes.
    - Accept finite nested instantiations of the same generic class while
    continuing to reject expanding recursive declarations. Generated
    readers/writers distinguish the complete child schema from self
    references, and generated class keys preserve that distinction.
    - Support parameterless Scala 3 enum keys in typed Scala maps. Reject
    unsupported case-class constructors through the Scala model instead of
    silently losing fields in the ordinary object path.
    - Complete the relevant coverage from #4067 across generated/interpreted
    operations, String/UTF-8 readers and writers, constructor shapes,
    collection implementations, enum keys, and generic types; update the
    Scala and Kotlin guides.
    
    ## Validation
    
    - Java JSON focused regression suites: 293 passed (`JsonContainerTest`,
    `JsonCreatorTest`, `JsonModuleTest`, `JsonGeneratedCapabilityKeyTest`,
    `JsonAnyPropertyTest`, `JsonUnwrappedTest`).
    - Scala JSON suites: 82 passed on Scala 2.13.18; 92 passed on Scala
    3.3.8.
    - Kotlin JSON suite: 122 passed on Kotlin 2.3.20.
    - Java Spotless/Checkstyle, Kotlin Spotless, Markdown Prettier, and `git
    diff --check` passed.
    - Local runtime validation used GraalVM JDK 21.0.11 in JVM mode.
    Native-image execution was not run locally.
    
    ## Related issues
    
    Fixes #4067
    Fixes #4068
    Fixes #4069
    
    ## Does this PR introduce any user-facing change?
    
    Missing Scala properties now use constructor or type defaults. Missing
    Kotlin properties use explicit defaults first, then zero/false for
    non-null scalars or null for nullable types. Kotlin non-null references
    still reject missing values. The previously failing Scala values and
    finite nested generic types now work as documented. No binary protocol
    change.
---
 .agents/languages/java.md                          |   5 +
 .agents/languages/kotlin.md                        |   3 +
 .agents/languages/scala.md                         |  13 +
 docs/json/kotlin.md                                |  11 +-
 docs/json/scala.md                                 |  66 +++-
 .../apache/fory/json/codec/JsonObjectModel.java    |  48 ++-
 .../apache/fory/json/codec/ObjectCodecBuilder.java |   7 +-
 .../org/apache/fory/json/codegen/JsonCodegen.java  |  21 +-
 .../json/codegen/JsonGeneratedCodecBuilder.java    |  39 ++-
 .../fory/json/codegen/JsonReaderCodegen.java       |  25 +-
 .../fory/json/codegen/JsonWriterCodegen.java       |  12 +-
 .../org/apache/fory/json/meta/JsonCreatorInfo.java |  42 ++-
 .../json/resolver/GeneratedCodecKeyBuilder.java    |   7 +
 .../fory/json/resolver/JsonTypeResolver.java       |  28 +-
 .../org/apache/fory/json/JsonContainerTest.java    |  87 ++++-
 .../fory/json/kotlin/KotlinMetadataModels.kt       |  31 ++
 .../apache/fory/json/kotlin/KotlinProductCodecs.kt |   2 +
 .../apache/fory/json/kotlin/ForyJsonKotlinTest.kt  |  48 +++
 .../json/kotlin/KotlinNullabilityRuntimeTest.kt    |  11 +-
 .../json/scala/internal/ScalaTypeRefMacros.scala   |   5 +
 .../json/scala/internal/ScalaTypeRefMacros.scala   |   5 +
 .../scala/internal/ScalaCollectionCodecs.scala     |  60 +++-
 .../fory/json/scala/internal/ScalaEnumCodec.scala  |  10 +-
 .../json/scala/internal/ScalaObjectModels.scala    | 107 +++++-
 .../json/scala/internal/ScalaScalarCodecs.scala    |   3 +-
 .../fory/json/scala/internal/ScalaTupleCodec.scala |   5 +-
 .../scala/internal/ScalaTypeCodecFactory.scala     |  21 ++
 .../fory/json/scala/ScalaJsonDerivationSuite.scala |  47 ++-
 .../apache/fory/json/scala/ScalaJsonSuite.scala    | 360 ++++++++++++++++++++-
 29 files changed, 990 insertions(+), 139 deletions(-)

diff --git a/.agents/languages/java.md b/.agents/languages/java.md
index f2fa2a6a1..deaa98a2e 100644
--- a/.agents/languages/java.md
+++ b/.agents/languages/java.md
@@ -18,6 +18,11 @@ Load this file when changing anything under `java/` or when Java drives a cross-
   tests solely because of that formatting pass. Verify formatting with `spotless:check` and inspect
   the diff/status instead.
 - Fory Java requires JDK `17+`.
+- JSON generated self calls must compare canonical ObjectCodec identity, not just the raw class.
+  Different instantiations such as Box<Box<Integer>> and Box<Integer> have different field codecs.
+  Apply the same rule to reader/writer dependency selection, any-properties, and unwrapped paths.
+  Generated class keys must distinguish self references from stored child capabilities because
+  they produce different constructor shapes, including for non-generic subtype occurrences.
 - Place regressions in the existing test class that owns the behavior and use semantic fixture and
   test names. Do not create standalone test classes named after issue numbers.
 - Run Java `spotless` with JDK `21+`. If the current runtime is lower than 21, export `JAVA_HOME` to a JDK 21 installation before running `mvn spotless:check` or `mvn spotless:apply`.
diff --git a/.agents/languages/kotlin.md b/.agents/languages/kotlin.md
index 25f81c890..f0cc372c5 100644
--- a/.agents/languages/kotlin.md
+++ b/.agents/languages/kotlin.md
@@ -4,6 +4,9 @@ Load this file when changing `kotlin/` or compiler code that generates Kotlin so
 
 ## Rules
 
+- Missing JSON constructor parameters use explicit Kotlin defaults first; otherwise non-null
+  numeric and Boolean parameters use zero and false, and nullable parameters use null. Other
+  non-null reference parameters remain required. Explicit null never requests a default.
 - Fory JSON Kotlin property inclusion follows the configured global or property-level rule,
   independently of constructor defaults and deferred initializers. Do not override or reject
   `NON_NULL` or `NON_EMPTY` to guarantee round trips. Missing-property defaults and nullability
diff --git a/.agents/languages/scala.md b/.agents/languages/scala.md
index ecfd3b07e..c048f4547 100644
--- a/.agents/languages/scala.md
+++ b/.agents/languages/scala.md
@@ -6,6 +6,19 @@ Load this file when changing `scala/`.
 
 - Run Scala commands from within `scala/`.
 - Scala serializers build on the Java implementation. If Java changed and the updated Java artifacts are not installed yet, run `cd ../java && mvn -T16 install -DskipTests` first.
+- Missing Scala JSON case-class constructor parameters use their type defaults: zero for numbers,
+  false for booleans, empty collections and arrays, None for Option, and null for other references.
+  Explicit constructor defaults take precedence. Mutable defaults must be fresh for each object.
+  Explicit JSON null keeps its existing decoding semantics. Preserve these rules in interpreted
+  and generated readers.
+- Scala JSON data occurrences of Unit use BoxedUnit, including ScalaTypeRef roots, nested
+  arguments, array components, and case-class properties. Normalize this at Scala type metadata
+  construction in both compiler versions and reuse ScalaUnitCodec's JSON null representation.
+  Do not special-case Option or change Java VoidCodec to accept Scala values. The Java Class
+  writer overload continues to reject void; Scala callers use ScalaTypeRef[Unit].
+- Recognize case classes independently of constructor support, including Scala 3 private copy
+  methods. Unsupported constructors must fail in the Scala model instead of falling through to
+  the ordinary object codec and silently dropping properties.
 - Scala supports the JVM and GraalVM Native Image, not Android. Do not add Android-specific Scala
   sources, tests, resources, R8 metadata, compiler plugins, macros, dependencies, or compatibility
   design.
diff --git a/docs/json/kotlin.md b/docs/json/kotlin.md
index 1b75007d4..42be9fb6a 100644
--- a/docs/json/kotlin.md
+++ b/docs/json/kotlin.md
@@ -137,7 +137,7 @@ For this model:
 
 - `{"id":1}` invokes both compiler defaults.
 - `{"id":1,"label":null}` passes an explicit null and does not invoke the `label` default.
-- a missing `id` fails before constructor invocation.
+- a missing `id` uses `0`.
 - `{"id":1,"retries":null}` fails; null never asks Kotlin to use a default.
 
 Normal body `var` properties preserve their initializer when absent and are assigned after
@@ -172,7 +172,9 @@ val text = json.toJson(Response(1, items = emptyList())) // {"id":1}
 
 Inclusion affects writing only. Reading the example's output uses the declared `items` default
 of null, so the original empty list is not preserved. A missing constructor parameter uses its
-declared default, or fails if it has no default; a missing body property keeps its initializer.
+declared default first. Otherwise, non-null numeric and Boolean parameters use zero and `false`,
+nullable parameters use `null`, and other non-null parameters remain required. A missing body
+property keeps its initializer.
 Explicit null remains distinct from a missing field and is rejected for a non-nullable property.
 Choose an inclusion rule that retains values when exact round trips are required.
 
@@ -190,7 +192,10 @@ and generic children:
 | Declaration                       | Missing member        | Explicit JSON `null` |
 | --------------------------------- | --------------------- | -------------------- |
 | `val value: String`               | Fails                 | Fails                |
-| `val value: String?`              | Fails                 | Passes null          |
+| `val value: String?`              | Passes null           | Passes null          |
+| `val value: Int`                  | Uses `0`              | Fails                |
+| `val value: Boolean`              | Uses `false`          | Fails                |
+| `val value: Int?`                 | Passes null           | Passes null          |
 | `val value: String = expression`  | Evaluates the default | Fails                |
 | `val value: String? = expression` | Evaluates the default | Passes null          |
 
diff --git a/docs/json/scala.md b/docs/json/scala.md
index 12fd656b4..74714f4f9 100644
--- a/docs/json/scala.md
+++ b/docs/json/scala.md
@@ -52,13 +52,34 @@ can erase Scala value-type arguments to `Object`.
 Case classes are decoded by calling their full primary constructor. Fory invokes Scala's generated
 constructor-default methods for missing defaulted parameters; it does not parse default expressions
 or mutate constructor `val` fields. Defaults in later parameter lists receive the preceding
-constructor arguments exactly as Scala defines them. A missing parameter without a default is an
-error. Mutable body properties are applied after construction.
+constructor arguments exactly as Scala defines them. When a constructor parameter has no explicit
+default, an omitted property uses its type's default:
+
+- Numeric values use zero, and Boolean values use `false`.
+- Collections, maps, and arrays use empty values. Mutable defaults are fresh for each object.
+- `Option[A]` uses `None`.
+- Other reference values, including strings and nested objects, use `null`.
+
+Mutable body properties retain their initializers when omitted and are applied after construction
+when present.
+
+```scala
+case class Options(value: Option[Int], selected: Option[Int] = Some(7))
+case class Profile(age: Int, enabled: Boolean, tags: List[String], name: String)
+
+json.fromJson("{}", classOf[Options]) // Options(None, Some(7))
+json.fromJson("""{"selected":null}""", classOf[Options]) // Options(None, None)
+json.fromJson("{}", classOf[Profile]) // Profile(0, false, List(), null)
+```
+
+Explicit constructor defaults take precedence for omitted properties. An explicit JSON `null`
+decodes as `None` for `Option[A]`, even when its constructor default is `Some(...)`.
 
 A case class may be declared at the top level, or inside an `object` at any nesting depth, as long
 as every enclosing scope is itself an `object`. A case class enclosed by a `class`, a trait, or a
 method is rejected for both reading and writing, because Fory cannot reach the enclosing instance
-or the companion it needs to rebuild the value.
+or the companion it needs to rebuild the value. Construction requires a public JVM constructor
+with a matching public companion `apply`; unsupported private constructor shapes are rejected.
 
 Fory JSON annotations can be placed directly on Scala constructor properties:
 
@@ -78,11 +99,10 @@ parameters. `JsonCodec` child slots bind direct collection elements, `Option` co
 or values. All other Fory JSON annotations retain the behavior described in
 [Annotations](annotations.md).
 
-If a required non-defaulted reference parameter uses an inclusion rule that would omit `null`,
-serialization rejects a null value. This guarantees that JSON written by Fory remains readable by
-the same case-class schema. A global `defaultPropertyInclusion(NON_EMPTY)` retains empty values of
-required constructor parameters. An explicit `@JsonProperty(include = NON_EMPTY)` is rejected for
-a required parameter whose type can be empty; add a constructor default to allow omission.
+Property inclusion controls which values are written. Omitted properties use constructor or type
+defaults when read, so omitting an empty string can restore `null`. Use `ALWAYS` when those values
+must remain distinct. An explicit JSON `null` keeps the declared type's normal null behavior; it
+does not request a constructor default.
 
 ## Supported Scala types
 
@@ -142,6 +162,33 @@ val rangeType = ScalaTypeRef[scala.collection.immutable.NumericRange[Int]]
 val range = json.fromJson("[1,3,5,7]", rangeType)
 ```
 
+Generic case classes preserve their type arguments, including finite nesting of the same class:
+
+```scala
+case class Box[A](value: A)
+
+val boxType = ScalaTypeRef[Box[Box[Int]]]
+val box = json.fromJson("""{"value":{"value":1}}""", boxType)
+json.toJson(box, boxType) // {"value":{"value":1}}
+```
+
+Recursive declarations that continually expand their type arguments, such as `Node[A]` containing
+`Node[List[A]]`, need a custom codec.
+
+Use `ScalaTypeRef[Unit]` for a `Unit` root value:
+
+```scala
+val unitType = ScalaTypeRef[Unit]
+json.toJson((), unitType)       // "null"
+json.fromJson("null", unitType) // ()
+```
+
+`Unit` also works in case-class fields and nested types such as `List[Unit]`, `Array[Unit]`,
+and `Option[Unit]`. Each `Unit` value is encoded as JSON `null`. Under the value-or-null
+representation of `Option`, `Some(())` writes `null` and reads back as `None`.
+Do not pass `classOf[Unit]` to the Java `Class` overload: it denotes JVM `void`, which is
+rejected when writing a root value.
+
 `Some[Int]` is a valid declared type when supplied with its complete type argument. A non-null JSON
 value decodes to `Some(value)`; JSON `null` is rejected for `Some[Int]` but decodes to `None` for
 `Option[Int]`.
@@ -257,7 +304,8 @@ non-finite number, and supported direct-wrapper behavior.
 
 ## Scala 3 closed enums and sealed hierarchies
 
-A parameterless Scala 3 enum uses its case name as a JSON string. Add `derives ScalaJsonCodec` to an
+A parameterless Scala 3 enum uses its case name as a JSON string, including as the key of a typed
+Scala map such as `Map[Color, String]`. Add `derives ScalaJsonCodec` to an
 enum with parameterized cases to define one closed wrapper-object representation for every case:
 
 ```scala
diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codec/JsonObjectModel.java b/java/fory-json/src/main/java/org/apache/fory/json/codec/JsonObjectModel.java
index 05a12bf67..f713c5f4b 100644
--- a/java/fory-json/src/main/java/org/apache/fory/json/codec/JsonObjectModel.java
+++ b/java/fory-json/src/main/java/org/apache/fory/json/codec/JsonObjectModel.java
@@ -30,6 +30,7 @@ import java.lang.reflect.WildcardType;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Objects;
+import java.util.function.Supplier;
 import org.apache.fory.annotation.Internal;
 import org.apache.fory.reflect.TypeRef;
 
@@ -43,6 +44,7 @@ public final class JsonObjectModel {
   private final Method[] accessors;
   private final Method[] defaultMethods;
   private final Object defaultsReceiver;
+  private final Supplier<?>[] defaultFactories;
   private final int[] defaultMaskBits;
   private final boolean[] parameterNullable;
   private final TypeRef<?>[] parameterTypes;
@@ -76,6 +78,7 @@ public final class JsonObjectModel {
         accessors,
         defaultMethods,
         null,
+        null,
         defaultMaskBits,
         parameterNullable,
         parameterTypes,
@@ -90,7 +93,10 @@ public final class JsonObjectModel {
    * {@code defaultsReceiver}. Scala emits {@code $lessinit$greater$default$N} on the companion
    * singleton and mirrors it as a static forwarder on the case class only for a top-level
    * companion, so a case class declared inside an {@code object} binds its defaults on that
-   * singleton. Pass {@code null} when the defaults are static members of the created type.
+   * singleton. A language module may also select a zero-argument static factory on the parameter's
+   * value type for an implicit default. Static defaults do not use {@code defaultsReceiver}; pass
+   * {@code null} when no instance default needs it. Type-default factories are evaluated only for
+   * missing parameters without a constructor default; mutable values must be newly allocated.
    */
   public JsonObjectModel(
       Constructor<?> constructor,
@@ -99,6 +105,7 @@ public final class JsonObjectModel {
       Method[] accessors,
       Method[] defaultMethods,
       Object defaultsReceiver,
+      Supplier<?>[] defaultFactories,
       int[] defaultMaskBits,
       boolean[] parameterNullable,
       TypeRef<?>[] parameterTypes,
@@ -114,6 +121,7 @@ public final class JsonObjectModel {
         accessors,
         defaultMethods,
         defaultsReceiver,
+        defaultFactories,
         defaultMaskBits,
         parameterNullable,
         parameterTypes,
@@ -148,6 +156,7 @@ public final class JsonObjectModel {
         accessors,
         defaultMethods,
         null,
+        null,
         defaultMaskBits,
         parameterNullable,
         parameterTypes,
@@ -175,7 +184,8 @@ public final class JsonObjectModel {
       Method[] propertySetters,
       TypeRef<?>[] propertyTypes,
       boolean[] propertyReconstructible,
-      boolean[] propertyRequired) {
+      boolean[] propertyRequired,
+      Supplier<?>[] defaultFactories) {
     this(
         creator,
         invocationCreator,
@@ -184,6 +194,7 @@ public final class JsonObjectModel {
         accessors,
         defaultMethods,
         null,
+        defaultFactories,
         defaultMaskBits,
         parameterNullable,
         parameterTypes,
@@ -203,6 +214,7 @@ public final class JsonObjectModel {
       Method[] accessors,
       Method[] defaultMethods,
       Object defaultsReceiver,
+      Supplier<?>[] defaultFactories,
       int[] defaultMaskBits,
       boolean[] parameterNullable,
       TypeRef<?>[] parameterTypes,
@@ -219,6 +231,7 @@ public final class JsonObjectModel {
     this.accessors = accessors.clone();
     this.defaultMethods = defaultMethods.clone();
     this.defaultsReceiver = defaultsReceiver;
+    this.defaultFactories = defaultFactories == null ? null : defaultFactories.clone();
     this.defaultMaskBits = defaultMaskBits.clone();
     this.parameterNullable = parameterNullable.clone();
     this.parameterTypes = parameterTypes.clone();
@@ -247,6 +260,7 @@ public final class JsonObjectModel {
     accessors = new Method[0];
     defaultMethods = new Method[0];
     defaultsReceiver = null;
+    defaultFactories = null;
     defaultMaskBits = new int[0];
     parameterNullable = new boolean[0];
     parameterTypes = new TypeRef<?>[0];
@@ -330,6 +344,7 @@ public final class JsonObjectModel {
     if (parameterNames.length != count
         || accessors.length != count
         || defaultMethods.length != count
+        || defaultFactories != null && defaultFactories.length != count
         || defaultMaskBits.length != count
         || parameterNullable.length != count
         || parameterTypes.length != count) {
@@ -372,7 +387,7 @@ public final class JsonObjectModel {
       }
     }
     HashSet<String> names = new HashSet<>();
-    boolean hasDefaultMethod = false;
+    boolean hasInstanceDefault = false;
     for (int i = 0; i < parameterNames.length; i++) {
       String name = parameterNames[i];
       if (name == null || name.isEmpty() || !names.add(name)) {
@@ -382,21 +397,15 @@ public final class JsonObjectModel {
       if (defaultMethods[i] != null && defaultMaskBits[i] >= 0) {
         throw new IllegalArgumentException("A constructor parameter has two default mechanisms");
       }
-      if (defaultMethods[i] != null
-          && Modifier.isStatic(defaultMethods[i].getModifiers()) == (defaultsReceiver != null)) {
-        throw new IllegalArgumentException(
-            "A JSON constructor default receiver is required exactly for instance defaults "
-                + defaultMethods[i]);
-      }
-      if (defaultsReceiver != null
-          && defaultMethods[i] != null
-          && !defaultMethods[i].getDeclaringClass().isInstance(defaultsReceiver)) {
-        throw new IllegalArgumentException(
-            "JSON constructor default receiver does not own " + defaultMethods[i]);
+      if (defaultMethods[i] != null && !Modifier.isStatic(defaultMethods[i].getModifiers())) {
+        if (!defaultMethods[i].getDeclaringClass().isInstance(defaultsReceiver)) {
+          throw new IllegalArgumentException(
+              "JSON constructor default receiver does not own " + defaultMethods[i]);
+        }
+        hasInstanceDefault = true;
       }
-      hasDefaultMethod |= defaultMethods[i] != null;
     }
-    if (defaultsReceiver != null && !hasDefaultMethod) {
+    if (defaultsReceiver != null && !hasInstanceDefault) {
       throw new IllegalArgumentException(
           "A JSON constructor default receiver requires at least one instance default");
     }
@@ -491,6 +500,13 @@ public final class JsonObjectModel {
     return defaultsReceiver;
   }
 
+  /**
+   * Returns type-default factories selected by the language module, or null if none are supplied.
+   */
+  public Supplier<?>[] defaultFactories() {
+    return defaultFactories == null ? null : defaultFactories.clone();
+  }
+
   public int[] defaultMaskBits() {
     return defaultMaskBits.clone();
   }
diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codec/ObjectCodecBuilder.java b/java/fory-json/src/main/java/org/apache/fory/json/codec/ObjectCodecBuilder.java
index ee8fb259a..6c2678035 100644
--- a/java/fory-json/src/main/java/org/apache/fory/json/codec/ObjectCodecBuilder.java
+++ b/java/fory-json/src/main/java/org/apache/fory/json/codec/ObjectCodecBuilder.java
@@ -36,6 +36,7 @@ import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.function.Supplier;
 import org.apache.fory.collection.Tuple2;
 import org.apache.fory.json.ForyJsonException;
 import org.apache.fory.json.PropertyNamingStrategy;
@@ -1664,6 +1665,7 @@ final class ObjectCodecBuilder {
     Executable creator = objectModel.creator();
     String[] names = objectModel.parameterNames();
     Method[] defaultMethods = objectModel.defaultMethods();
+    Supplier<?>[] defaultFactories = objectModel.defaultFactories();
     int[] defaultMaskBits = objectModel.defaultMaskBits();
     TypeRef<?>[] logicalParameterTypes = objectModel.parameterTypes();
     Type[] parameterTypes = creator.getGenericParameterTypes();
@@ -1715,7 +1717,9 @@ final class ObjectCodecBuilder {
       bindCreatorType(ownerType, creator, i, parameterTypes[i], builder);
       builder.mergeCreatorParameter(type, parameters[i]);
       if (!builder.creatorReadAllowed()) {
-        if (defaultMethods[i] == null && defaultMaskBits[i] < 0) {
+        if (defaultMethods[i] == null
+            && defaultMaskBits[i] < 0
+            && (defaultFactories == null || defaultFactories[i] == null)) {
           throw new ForyJsonException(
               "Ignored constructor property " + names[i] + " requires a language default");
         }
@@ -1746,6 +1750,7 @@ final class ObjectCodecBuilder {
         generatedCodec,
         defaultMethods,
         objectModel.defaultsReceiver(),
+        defaultFactories,
         names,
         objectModel.defaultConstructor(),
         defaultMaskBits,
diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonCodegen.java b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonCodegen.java
index 5ff4c69a4..462709f1c 100644
--- a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonCodegen.java
+++ b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonCodegen.java
@@ -1134,10 +1134,9 @@ public final class JsonCodegen {
         return false;
       }
       Method defaultMethod = creator.defaultMethod(i);
-      // JsonCreatorInfo guarantees that a default method belongs to the creator owner, or to the
-      // language singleton that owns instance defaults, and that its dependency types are the
-      // preceding creator parameters. The generated reader invokes that exact method on that exact
-      // declaring class, so validate its access from the final definition context as well.
+      // JsonCreatorInfo validates compiler defaults and module-selected value factories. Their
+      // dependencies, if any, are preceding creator parameters. The generated reader invokes that
+      // exact method on its declaring class, so validate access from the final definition context.
       if (defaultMethod != null && !canCall(defaultMethod)) {
         return false;
       }
@@ -1346,14 +1345,15 @@ public final class JsonCodegen {
     if (any == null || any.readField() == null && any.readSetter() == null) {
       return false;
     }
-    if (storesSelfReader(
-        owner.type(), owner.readFields(), owner.creatorInfo() != null, any, resolver)) {
+    if (storesSelfReader(owner, owner.readFields(), owner.creatorInfo() != null, any, resolver)) {
       return true;
     }
     JsonUnwrappedInfo unwrapped = owner.unwrappedInfo();
     if (unwrapped != null) {
       for (JsonUnwrappedInfo.ReadRoute route : unwrapped.readRoutes()) {
-        if (route.field() != null && readNestedType(route.field(), resolver) == owner.type()) {
+        if (route.field() != null
+            && readNestedType(route.field(), resolver) != null
+            && resolver.canonicalObjectCodec(route.field().readTypeInfo()) == owner) {
           return true;
         }
       }
@@ -1362,19 +1362,20 @@ public final class JsonCodegen {
   }
 
   static boolean storesSelfReader(
-      Class<?> type,
+      ObjectCodec<?> owner,
       JsonFieldInfo[] properties,
       boolean creator,
       AnyInfo any,
       JsonTypeResolver resolver) {
-    if (any.valueRawType() == type && resolver.canonicalObjectCodec(any.valueTypeInfo()) != null) {
+    if (resolver.canonicalObjectCodec(any.valueTypeInfo()) == owner) {
       return true;
     }
     if (creator) {
       return false;
     }
     for (JsonFieldInfo property : properties) {
-      if (readNestedType(property, resolver) == type) {
+      if (readNestedType(property, resolver) != null
+          && resolver.canonicalObjectCodec(property.readTypeInfo()) == owner) {
         return true;
       }
     }
diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonGeneratedCodecBuilder.java b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonGeneratedCodecBuilder.java
index 8020c5cdf..5e1b148f3 100644
--- a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonGeneratedCodecBuilder.java
+++ b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonGeneratedCodecBuilder.java
@@ -104,23 +104,42 @@ final class JsonGeneratedCodecBuilder extends CodecBuilder {
 
   Expression fieldValue(JsonFieldInfo property, Expression object) {
     Method getter = property.writeGetter();
+    Expression value;
     if (getter != null) {
+      // Scala 3 can emit a BoxedUnit descriptor with a void generic return signature.
+      TypeRef<?> returnType =
+          TypeRef.of(
+              getter.getGenericReturnType() == void.class
+                  ? getter.getReturnType()
+                  : getter.getGenericReturnType());
       // JSON writers check the returned member value directly. Requesting expression-level null
       // state here only emits an unused boolean for each nullable getter and bloats generated
       // object writers enough to hurt C2 inlining.
+      Expression invocation;
       if (DirectMethodCodegen.sourceNameable(getter)) {
-        return new Expression.Invoke(
-            object,
-            getter.getName(),
-            property.name(),
-            TypeRef.of(getter.getGenericReturnType()),
-            false);
+        invocation =
+            new Expression.Invoke(object, getter.getName(), property.name(), returnType, false);
+      } else {
+        String name = DirectMethodCodegen.getterName(getter);
+        addDirectMethod(name, getter.getReturnType(), getter.getDeclaringClass(), "target");
+        invocation = directInvoke(name, property.name(), returnType, object);
       }
-      String name = DirectMethodCodegen.getterName(getter);
-      addDirectMethod(name, getter.getReturnType(), getter.getDeclaringClass(), "target");
-      return directInvoke(name, property.name(), TypeRef.of(getter.getGenericReturnType()), object);
+      // Scala Unit getters can return void. Match reflective access, which adapts their absent
+      // result to null for the logical Unit codec, rather than generating a cast from void.
+      value =
+          getter.getReturnType() == void.class
+              ? new Expression.ListExpression(
+                  invocation, new Expression.Null(TypeRef.of(property.writeRawType()), false))
+              : invocation;
+    } else {
+      value = getFieldValue(object, writeDescriptor(property));
     }
-    return getFieldValue(object, writeDescriptor(property));
+    // A resolved primitive type argument still has an erased Object accessor or field.
+    // Adapt it here so both fused object-start writes and ordinary fields receive primitives.
+    Class<?> writeType = property.writeRawType();
+    return writeType.isPrimitive() && writeType != void.class && !value.type().isPrimitive()
+        ? new Expression.Cast(value, TypeRef.of(writeType))
+        : value;
   }
 
   Expression anyValue(Field field, Expression object) {
diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonReaderCodegen.java b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonReaderCodegen.java
index 528f6c206..b946e70ac 100644
--- a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonReaderCodegen.java
+++ b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonReaderCodegen.java
@@ -260,7 +260,7 @@ abstract class JsonReaderCodegen {
     this.any = any;
     ownerType = type;
     storesSelfReader =
-        JsonCodegen.storesSelfReader(type, properties, creatorInfo != null, any, resolver);
+        JsonCodegen.storesSelfReader(owner, properties, creatorInfo != null, any, resolver);
     if (creatorInfo != null) {
       return genAnyCreatorReaderCode(builder, type, creatorInfo);
     }
@@ -1494,11 +1494,13 @@ abstract class JsonReaderCodegen {
       if (method == null) {
         int maskBit = creator.defaultMaskBit(i);
         if (maskBit < 0) {
-          body.append("throw ")
+          body.append("arguments[")
+              .append(i)
+              .append("] = ")
               .append(creatorExpression)
-              .append(".missingArgument(")
+              .append(".defaultValue(")
               .append(i)
-              .append(");\n");
+              .append(", arguments);\n");
         } else {
           body.append("arguments[")
               .append(i)
@@ -1625,7 +1627,11 @@ abstract class JsonReaderCodegen {
     if (type == char.class) {
       return Expression.Literal.ofChar((char) 0);
     }
-    return new Expression.Literal(type == float.class ? 0F : 0D, TypeRef.of(type));
+    // A numeric conditional would promote 0F to Double before boxing the literal.
+    if (type == float.class) {
+      return new Expression.Literal(0F, TypeRef.of(type));
+    }
+    return new Expression.Literal(0D, TypeRef.of(type));
   }
 
   private Expression readCreatorValue(
@@ -4159,8 +4165,8 @@ abstract class JsonReaderCodegen {
   }
 
   private boolean storesAnyReader(Class<?> type) {
-    return resolver.canonicalObjectCodec(any.valueTypeInfo()) == null
-        || any.valueTypeInfo().rawType() != type;
+    return any.valueTypeInfo().rawType() != type
+        || resolver.canonicalObjectCodec(any.valueTypeInfo()) != objectOwner;
   }
 
   private Expression anyReaderRef() {
@@ -4571,7 +4577,9 @@ abstract class JsonReaderCodegen {
 
   final boolean storesReadObjectCodec(Class<?> type, JsonFieldInfo property) {
     Class<?> nestedType = readNestedType(property);
-    return nestedType != null && nestedType != type;
+    return nestedType != null
+        && (nestedType != type
+            || resolver.canonicalObjectCodec(property.readTypeInfo()) != objectOwner);
   }
 
   private Expression readField(
@@ -4845,6 +4853,7 @@ abstract class JsonReaderCodegen {
   final Expression readObjectValue(Class<?> type, JsonFieldInfo property, int id) {
     Expression codec =
         property.readRawType() == type
+                && resolver.canonicalObjectCodec(property.readTypeInfo()) == objectOwner
             ? nestedSelfReaderRef()
             : usesReaderSlot(property.readTypeInfo())
                 ? readerFromSlot(fieldRef("o" + id, JsonTypeInfo.class))
diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonWriterCodegen.java b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonWriterCodegen.java
index 0d2bd726b..ce60b9d17 100644
--- a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonWriterCodegen.java
+++ b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonWriterCodegen.java
@@ -847,8 +847,7 @@ abstract class JsonWriterCodegen {
   }
 
   private boolean storesAnyWriter(AnyInfo any) {
-    return resolver.canonicalObjectCodec(any.valueTypeInfo()) == null
-        || any.valueRawType() != ownerType;
+    return resolver.canonicalObjectCodec(any.valueTypeInfo()) != objectOwner;
   }
 
   static final class PrefixFields {
@@ -1650,10 +1649,10 @@ abstract class JsonWriterCodegen {
 
   private Expression writeCodec(
       JsonFieldInfo property, int id, Expression value, Expression writer) {
-    boolean object = resolver.canonicalObjectCodec(property.writeTypeInfo()) != null;
-    Class<?> valueType = property.writeTypeInfo().rawType();
+    // Equal raw classes can have different generic bindings. Only the canonical object owner
+    // identifies a self edge; Box<Box<Integer>> must retain the child Box<Integer> writer.
     Expression codec =
-        object && valueType == ownerType
+        resolver.canonicalObjectCodec(property.writeTypeInfo()) == objectOwner
             ? new Reference("this", TypeRef.of(completeWriterType()))
             : usesWriterSlot(property)
                 ? writerFromSlot(fieldRef("w" + id, JsonTypeInfo.class))
@@ -1668,8 +1667,7 @@ abstract class JsonWriterCodegen {
 
   private boolean storesWriteCodec(JsonFieldInfo property) {
     return usesWriteCodec(property)
-        && (resolver.canonicalObjectCodec(property.writeTypeInfo()) == null
-            || property.writeTypeInfo().rawType() != ownerType);
+        && resolver.canonicalObjectCodec(property.writeTypeInfo()) != objectOwner;
   }
 
   private boolean usesWriterSlot(JsonFieldInfo property) {
diff --git a/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonCreatorInfo.java b/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonCreatorInfo.java
index 0a9151c05..f745db4df 100644
--- a/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonCreatorInfo.java
+++ b/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonCreatorInfo.java
@@ -29,6 +29,7 @@ import java.lang.reflect.Method;
 import java.util.Arrays;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ConcurrentMap;
+import java.util.function.Supplier;
 import org.apache.fory.annotation.Internal;
 import org.apache.fory.collection.ClassValueCache;
 import org.apache.fory.json.ForyJsonException;
@@ -69,6 +70,7 @@ public final class JsonCreatorInfo {
   private final GeneratedJsonCodec<?> generatedCodec;
   private final Method[] defaultMethods;
   private final Object defaultsReceiver;
+  private final Supplier<?>[] defaultFactories;
   private final MethodHandle[] defaultInvokers;
   private final Constructor<?> defaultConstructor;
   private final MethodHandle defaultConstructorInvoker;
@@ -100,6 +102,7 @@ public final class JsonCreatorInfo {
         null,
         null,
         null,
+        null,
         null);
   }
 
@@ -113,6 +116,7 @@ public final class JsonCreatorInfo {
       GeneratedJsonCodec<?> generatedCodec,
       Method[] defaultMethods,
       Object defaultsReceiver,
+      Supplier<?>[] defaultFactories,
       String[] parameterNames,
       Constructor<?> defaultConstructor,
       int[] defaultMaskBits,
@@ -126,6 +130,7 @@ public final class JsonCreatorInfo {
         generatedCodec,
         defaultMethods,
         defaultsReceiver,
+        defaultFactories,
         parameterNames,
         defaultConstructor,
         defaultMaskBits,
@@ -148,6 +153,7 @@ public final class JsonCreatorInfo {
         null,
         null,
         null,
+        null,
         instance);
   }
 
@@ -166,6 +172,7 @@ public final class JsonCreatorInfo {
       GeneratedJsonCodec<?> generatedCodec,
       Method[] defaultMethods,
       Object defaultsReceiver,
+      Supplier<?>[] defaultFactories,
       String[] parameterNames,
       Constructor<?> defaultConstructor,
       int[] defaultMaskBits,
@@ -186,6 +193,7 @@ public final class JsonCreatorInfo {
     this.parameterNames = parameterNames == null ? null : parameterNames.clone();
     this.defaultMethods = defaultMethods == null ? null : defaultMethods.clone();
     this.defaultsReceiver = defaultsReceiver;
+    this.defaultFactories = defaultFactories == null ? null : defaultFactories.clone();
     defaultInvokers =
         this.defaultMethods == null
             ? null
@@ -225,6 +233,7 @@ public final class JsonCreatorInfo {
     generatedCodec = source.generatedCodec;
     defaultMethods = source.defaultMethods;
     defaultsReceiver = source.defaultsReceiver;
+    defaultFactories = source.defaultFactories;
     defaultInvokers = source.defaultInvokers;
     defaultConstructor = source.defaultConstructor;
     defaultConstructorInvoker = source.defaultConstructorInvoker;
@@ -455,7 +464,8 @@ public final class JsonCreatorInfo {
   @Internal
   public boolean hasDefault(int index) {
     return defaultInvokers != null && defaultInvokers[index] != null
-        || defaultMaskBits != null && defaultMaskBits[index] >= 0;
+        || defaultMaskBits != null && defaultMaskBits[index] >= 0
+        || defaultFactories != null && defaultFactories[index] != null;
   }
 
   /** Returns one prevalidated language-defined constructor default method. */
@@ -475,6 +485,9 @@ public final class JsonCreatorInfo {
   public Object defaultValue(int index, Object[] arguments) {
     MethodHandle invoker = defaultInvokers == null ? null : defaultInvokers[index];
     if (invoker == null) {
+      if (defaultFactories != null && defaultFactories[index] != null) {
+        return defaultFactories[index].get();
+      }
       throw missingArgument(index);
     }
     try {
@@ -562,9 +575,10 @@ public final class JsonCreatorInfo {
       if (argument == MISSING) {
         int bit = defaultMaskBits[i];
         if (bit < 0) {
-          throw missingArgument(i);
+          arguments[i] = defaultValue(i, arguments);
+        } else {
+          useDefault = true;
         }
-        useDefault = true;
       } else if (argument == null
           && parameterNullable != null
           && !parameterNullable[i]
@@ -668,16 +682,22 @@ public final class JsonCreatorInfo {
       if (method == null) {
         continue;
       }
-      // A default is either a static member of the created type or an instance member of the
-      // language singleton that owns it, such as a Scala companion of a nested case class.
+      // Language modules select defaults during schema construction. Compiler defaults belong to
+      // the creator or its companion; implicit defaults can use a value type's static factory,
+      // such as Option.empty, alongside those compiler defaults.
       boolean instanceDefault = !java.lang.reflect.Modifier.isStatic(method.getModifiers());
       Class<?> declaringClass = method.getDeclaringClass();
-      if ((instanceDefault
-              ? defaultsReceiver == null
-                  || !declaringClass.isInstance(defaultsReceiver)
-                  || !declaringClass.getName().equals(ownerType.getName() + "$")
-              : defaultsReceiver != null || declaringClass != ownerType)
-          || !method.getName().equals("$lessinit$greater$default$" + (i + 1))
+      boolean constructorDefault =
+          method.getName().equals("$lessinit$greater$default$" + (i + 1))
+              && (instanceDefault
+                  ? declaringClass.isInstance(defaultsReceiver)
+                      && declaringClass.getName().equals(ownerType.getName() + "$")
+                  : declaringClass == ownerType);
+      boolean valueFactory =
+          !instanceDefault
+              && method.getParameterCount() == 0
+              && method.getReturnType() == declaringClass;
+      if ((!constructorDefault && !valueFactory)
           || method.getParameterCount() > i
           || !java.lang.reflect.Modifier.isPublic(method.getModifiers())
           || !boxed(parameterTypes[i]).isAssignableFrom(boxed(method.getReturnType()))) {
diff --git a/java/fory-json/src/main/java/org/apache/fory/json/resolver/GeneratedCodecKeyBuilder.java b/java/fory-json/src/main/java/org/apache/fory/json/resolver/GeneratedCodecKeyBuilder.java
index 6102357e3..df4fa3999 100644
--- a/java/fory-json/src/main/java/org/apache/fory/json/resolver/GeneratedCodecKeyBuilder.java
+++ b/java/fory-json/src/main/java/org/apache/fory/json/resolver/GeneratedCodecKeyBuilder.java
@@ -49,6 +49,7 @@ final class GeneratedCodecKeyBuilder {
     EXACT_CODEC,
     FACTORY,
     MIXIN,
+    SELF_REFERENCE,
     CYCLE_SLOT
   }
 
@@ -168,6 +169,12 @@ final class GeneratedCodecKeyBuilder {
   }
 
   private void addRegistration(JsonTypeInfo typeInfo) {
+    // Self calls and stored child capabilities have different generated constructor shapes.
+    // Subtype occurrences can have distinct owners even when their raw classes are identical.
+    if (resolver.canonicalObjectCodec(typeInfo) == owner) {
+      keyParts.add(Part.SELF_REFERENCE);
+      keyParts.add(occurrence);
+    }
     JsonSharedRegistry registry = resolver.sharedRegistry();
     String factoryKey = typeInfo.factoryKey();
     if (factoryKey != null) {
diff --git a/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonTypeResolver.java b/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonTypeResolver.java
index 38329039a..8a919f98a 100644
--- a/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonTypeResolver.java
+++ b/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonTypeResolver.java
@@ -226,7 +226,7 @@ public final class JsonTypeResolver {
   }
 
   /**
-   * Returns the stable metadata owner for a canonical raw-class object binding, or {@code null}.
+   * Returns the stable metadata owner for a complete declared-type object binding, or {@code null}.
    *
    * <p>This lookup never constructs metadata or requests compilation. Source generation may call it
    * outside a root operation; the short resolver-local lock protects the ordinary owner maps
@@ -1829,7 +1829,7 @@ public final class JsonTypeResolver {
   }
 
   private boolean storesAnyCodec(ObjectCodec<?> owner, AnyInfo any) {
-    return canonicalObjectCodec(any.valueTypeInfo()) == null || any.valueRawType() != owner.type();
+    return canonicalObjectCodec(any.valueTypeInfo()) != owner;
   }
 
   enum CapabilityKind {
@@ -1846,9 +1846,7 @@ public final class JsonTypeResolver {
         utf8Writer
             ? JsonCodegen.usesUtf8WriteCodec(field, this)
             : JsonCodegen.usesWriteCodec(field);
-    return usesCodec
-        && (field.writeRawType() != owner.type()
-            || canonicalObjectOwner(field.writeTypeInfo()) == null);
+    return usesCodec && canonicalObjectOwner(field.writeTypeInfo()) != owner;
   }
 
   private boolean storesReadCapability(ObjectCodec<?> owner, JsonFieldInfo field) {
@@ -1856,7 +1854,7 @@ public final class JsonTypeResolver {
       return true;
     }
     Class<?> nestedType = JsonCodegen.readNestedType(field, this);
-    return nestedType != null && nestedType != owner.type();
+    return nestedType != null && canonicalObjectOwner(field.readTypeInfo()) != owner;
   }
 
   private ArrayList<JsonTypeInfo> capabilityChildren(ObjectCodec<?> owner, CapabilityKind kind) {
@@ -2727,12 +2725,28 @@ public final class JsonTypeResolver {
       activeGenericBindings.put(rawType, type);
       return true;
     }
-    if (!active.getTypeArguments().equals(type.getTypeArguments())) {
+    // A nested instantiation already contained in the active declaration contracts a finite
+    // type tree: Box<Box<Integer>> can bind Box<Integer>. Keep the outer binding as the owner
+    // so every admitted instantiation stays within that finite tree; an expanding declaration
+    // such as Node<T> -> Node<List<T>> still cannot introduce a new instantiation recursively.
+    if (!active.getTypeArguments().equals(type.getTypeArguments()) && !containsType(active, type)) {
       throw expandingGenericType(rawType, active, type);
     }
     return false;
   }
 
+  private static boolean containsType(TypeRef<?> enclosing, TypeRef<?> nested) {
+    if (enclosing.getType().equals(nested.getType())) {
+      return true;
+    }
+    for (TypeRef<?> argument : enclosing.getTypeArguments()) {
+      if (containsType(argument, nested)) {
+        return true;
+      }
+    }
+    return enclosing.isArray() && containsType(enclosing.getComponentType(), nested);
+  }
+
   private void exitObjectBinding(TypeRef<?> type, boolean owner) {
     if (owner) {
       activeGenericBindings.remove(type.getRawType());
diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonContainerTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonContainerTest.java
index 2665b8a52..8bf51f717 100644
--- a/java/fory-json/src/test/java/org/apache/fory/json/JsonContainerTest.java
+++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonContainerTest.java
@@ -69,6 +69,7 @@ import java.util.concurrent.LinkedBlockingQueue;
 import java.util.concurrent.atomic.AtomicIntegerArray;
 import java.util.concurrent.atomic.AtomicLongArray;
 import java.util.concurrent.atomic.AtomicReferenceArray;
+import org.apache.fory.json.annotation.JsonAnyProperty;
 import org.apache.fory.json.codec.ArrayCodec;
 import org.apache.fory.json.codec.JsonValueCodec;
 import org.apache.fory.json.codec.MapCodec;
@@ -374,15 +375,83 @@ public class JsonContainerTest extends ForyJsonTestModels {
         generatedUtf8WriterClass(json, strings), generatedUtf8WriterClass(json, values));
   }
 
+  @Test
+  public void nestedGenericPojo() {
+    for (boolean codegen : new boolean[] {false, true}) {
+      ForyJson json = newJson(codegen);
+      TypeRef<GenericBox<GenericBox<Integer>>> type =
+          new TypeRef<GenericBox<GenericBox<Integer>>>() {};
+      GenericBox<GenericBox<Integer>> outer = new GenericBox<>();
+      outer.value = new GenericBox<>();
+      outer.value.value = 1;
+      outer.value.values = Arrays.asList(2, 3);
+      outer.values = Collections.singletonList(outer.value);
+      String encoded = json.toJson(outer, type);
+      assertEquals(new String(json.toJsonBytes(outer, type), StandardCharsets.UTF_8), encoded);
+      for (GenericBox<GenericBox<Integer>> decoded :
+          Arrays.asList(
+              json.fromJson(encoded, type),
+              json.fromJson(encoded.getBytes(StandardCharsets.UTF_8), type))) {
+        assertEquals(decoded.value.value, Integer.valueOf(1));
+        assertEquals(decoded.value.values, Arrays.asList(2, 3));
+        assertEquals(decoded.values.get(0).value, Integer.valueOf(1));
+      }
+      TypeRef<GenericBox<List<GenericBox<Integer>>>> listType =
+          new TypeRef<GenericBox<List<GenericBox<Integer>>>>() {};
+      GenericBox<List<GenericBox<Integer>>> list =
+          json.fromJson("{\"value\":[{\"value\":4}]}", listType);
+      assertEquals(list.value.get(0).value, Integer.valueOf(4));
+      assertEquals(
+          json.fromJson(json.toJsonBytes(list, listType), listType).value.get(0).value,
+          Integer.valueOf(4));
+      TypeRef<GenericBox<GenericBox<Integer>[]>> arrayType =
+          new TypeRef<GenericBox<GenericBox<Integer>[]>>() {};
+      GenericBox<GenericBox<Integer>[]> array =
+          json.fromJson("{\"value\":[{\"value\":5}]}", arrayType);
+      assertEquals(array.value[0].value, Integer.valueOf(5));
+      assertEquals(
+          json.fromJson(json.toJsonBytes(array, arrayType), arrayType).value[0].value,
+          Integer.valueOf(5));
+    }
+  }
+
+  @Test
+  public void nestedGenericAnyProperties() {
+    ForyJson json = newJson();
+    TypeRef<GenericProperties<GenericProperties<Integer>>> type =
+        new TypeRef<GenericProperties<GenericProperties<Integer>>>() {};
+    String text = "{\"nested\":{\"answer\":42}}";
+    GenericProperties<GenericProperties<Integer>> value = json.fromJson(text, type);
+    assertEquals(value.properties.get("nested").properties.get("answer"), Integer.valueOf(42));
+    assertEquals(json.toJson(value, type), text);
+    assertEquals(new String(json.toJsonBytes(value, type), StandardCharsets.UTF_8), text);
+    assertEquals(
+        json.fromJson(text.getBytes(StandardCharsets.UTF_8), type)
+            .properties
+            .get("nested")
+            .properties
+            .get("answer"),
+        Integer.valueOf(42));
+  }
+
   @Test
   public void rejectExpandingGeneric() {
-    ForyJson json = newJson(true);
-    assertThrows(
-        ForyJsonException.class,
-        () -> json.fromJson("{\"next\":null}", new TypeRef<Expanding<String>>() {}));
-    GenericBox<String> value =
-        json.fromJson("{\"value\":\"ready\",\"values\":[]}", new TypeRef<GenericBox<String>>() {});
-    assertEquals(value.value, "ready");
+    for (boolean codegen : new boolean[] {false, true}) {
+      ForyJson json = newJson(codegen);
+      for (int attempt = 0; attempt < 2; attempt++) {
+        assertThrows(
+            ForyJsonException.class,
+            () -> json.fromJson("{\"next\":null}", new TypeRef<Expanding<String>>() {}));
+        assertThrows(
+            ForyJsonException.class,
+            () -> json.fromJson("{}", new TypeRef<Expanding<Expanding<String>>>() {}));
+        GenericBox<GenericBox<String>> value =
+            json.fromJson(
+                "{\"value\":{\"value\":\"ready\"}}",
+                new TypeRef<GenericBox<GenericBox<String>>>() {});
+        assertEquals(value.value.value, "ready");
+      }
+    }
   }
 
   @Test
@@ -1183,6 +1252,10 @@ public class JsonContainerTest extends ForyJsonTestModels {
     public Expanding<List<T>> next;
   }
 
+  public static final class GenericProperties<T> {
+    @JsonAnyProperty public Map<String, T> properties;
+  }
+
   public static final class NoteList extends ArrayList<Note> {}
 
   public static final class Note {
diff --git a/kotlin/fory-json-kotlin/src/main/kotlin/org/apache/fory/json/kotlin/KotlinMetadataModels.kt b/kotlin/fory-json-kotlin/src/main/kotlin/org/apache/fory/json/kotlin/KotlinMetadataModels.kt
index be68e4738..90bfb59e1 100644
--- a/kotlin/fory-json-kotlin/src/main/kotlin/org/apache/fory/json/kotlin/KotlinMetadataModels.kt
+++ b/kotlin/fory-json-kotlin/src/main/kotlin/org/apache/fory/json/kotlin/KotlinMetadataModels.kt
@@ -26,6 +26,7 @@ import java.lang.reflect.Field
 import java.lang.reflect.Method
 import java.lang.reflect.Modifier
 import java.util.LinkedHashMap
+import java.util.function.Supplier
 import kotlin.ExperimentalContextParameters
 import kotlin.metadata.ClassKind
 import kotlin.metadata.ExperimentalContextReceivers
@@ -195,9 +196,39 @@ internal object KotlinMetadataModels {
       properties.map { it.type }.toTypedArray(),
       BooleanArray(properties.size) { properties[it].reconstructible },
       BooleanArray(properties.size) { properties[it].required },
+      Array(parameters.size) {
+        if (defaultMaskBits[it] < 0) typeDefault(parameterTypes[it]) else null
+      },
     )
   }
 
+  internal fun typeDefault(type: TypeRef<*>): Supplier<*>? {
+    // Nullable parameters default to null even when their non-null type has a scalar zero value.
+    // Other non-null references still require a constructor default or an explicit JSON property.
+    if (nullable(type)) return Supplier { null }
+    val value: Any =
+      when (type.rawType) {
+        java.lang.Boolean.TYPE,
+        java.lang.Boolean::class.java -> false
+        java.lang.Byte.TYPE,
+        java.lang.Byte::class.java -> 0.toByte()
+        java.lang.Short.TYPE,
+        java.lang.Short::class.java -> 0.toShort()
+        java.lang.Integer.TYPE,
+        java.lang.Integer::class.java -> 0
+        java.lang.Long.TYPE,
+        java.lang.Long::class.java -> 0L
+        java.lang.Float.TYPE,
+        java.lang.Float::class.java -> 0F
+        java.lang.Double.TYPE,
+        java.lang.Double::class.java -> 0.0
+        java.lang.Character.TYPE,
+        java.lang.Character::class.java -> '\u0000'
+        else -> return null
+      }
+    return Supplier { value }
+  }
+
   private fun selectCreator(
     ownerType: TypeRef<*>,
     rawType: Class<*>,
diff --git a/kotlin/fory-json-kotlin/src/main/kotlin/org/apache/fory/json/kotlin/KotlinProductCodecs.kt b/kotlin/fory-json-kotlin/src/main/kotlin/org/apache/fory/json/kotlin/KotlinProductCodecs.kt
index 45202cf3e..d8234ccf5 100644
--- a/kotlin/fory-json-kotlin/src/main/kotlin/org/apache/fory/json/kotlin/KotlinProductCodecs.kt
+++ b/kotlin/fory-json-kotlin/src/main/kotlin/org/apache/fory/json/kotlin/KotlinProductCodecs.kt
@@ -84,6 +84,8 @@ internal object KotlinProductCodecs {
       names,
       accessors,
       arrayOfNulls(names.size),
+      null,
+      Array(names.size) { KotlinMetadataModels.typeDefault(types[it]) },
       IntArray(names.size) { -1 },
       BooleanArray(names.size) { nullable(types[it]) },
       types,
diff --git a/kotlin/fory-json-kotlin/src/test/kotlin/org/apache/fory/json/kotlin/ForyJsonKotlinTest.kt b/kotlin/fory-json-kotlin/src/test/kotlin/org/apache/fory/json/kotlin/ForyJsonKotlinTest.kt
index 1dbf15849..ceb78d486 100644
--- a/kotlin/fory-json-kotlin/src/test/kotlin/org/apache/fory/json/kotlin/ForyJsonKotlinTest.kt
+++ b/kotlin/fory-json-kotlin/src/test/kotlin/org/apache/fory/json/kotlin/ForyJsonKotlinTest.kt
@@ -36,6 +36,20 @@ class ForyJsonKotlinTest {
 
   data class Required(val id: Long, val name: String)
 
+  data class MissingValues(
+    val age: Int,
+    val enabled: Boolean,
+    val optionalAge: Int?,
+    val optionalName: String?,
+    val name: String = "guest",
+    val byte: Byte,
+    val short: Short,
+    val long: Long,
+    val float: Float,
+    val double: Double,
+    val char: Char,
+  )
+
   data class EvaluatedDefault(
     val seed: Int,
     val values: MutableList<Int> = mutableListOf(nextDefault++),
@@ -107,6 +121,40 @@ class ForyJsonKotlinTest {
     }
   }
 
+  @Test
+  fun missingTypeDefaults() {
+    for (codegen in listOf(false, true)) {
+      val json = ForyJsonKotlin.builder().withCodegen(codegen).withAsyncCompilation(false).build()
+      val expected = MissingValues(0, false, null, null, "guest", 0, 0, 0L, 0F, 0.0, '\u0000')
+      val type = jsonTypeRef<MissingValues>()
+      for (text in listOf("{}", """{"unknown":"中"}""")) {
+        assertEquals(expected, json.fromJson(text, type))
+        assertEquals(expected, json.fromJson(text.toByteArray(Charsets.UTF_8), type))
+      }
+      assertEquals(
+        expected.copy(
+          age = 8,
+          enabled = true,
+          optionalAge = 4,
+          optionalName = "ready",
+          name = "member"
+        ),
+        json.fromJson(
+          """{"age":8,"enabled":true,"optionalAge":4,"optionalName":"ready","name":"member"}""",
+          type
+        ),
+      )
+      assertEquals(ReferencedDefault(0, 1), json.fromJson("{}", jsonTypeRef<ReferencedDefault>()))
+      assertEquals(Box<Int?>(null), json.fromJson("{}", jsonTypeRef<Box<Int?>>()))
+      assertEquals(Box(0), json.fromJson("{}", jsonTypeRef<Box<Int>>()))
+      assertEquals(0 to null, json.fromJson("{}", jsonTypeRef<Pair<Int, String?>>()))
+      assertFailsWith<ForyJsonException> { json.fromJson("{}", jsonTypeRef<Required>()) }
+      assertFailsWith<ForyJsonException> { json.fromJson("""{"age":null}""", type) }
+      assertFailsWith<ForyJsonException> { json.fromJson("""{"name":null}""", type) }
+      assertEquals(expected, json.fromJson("{}", type))
+    }
+  }
+
   @Test
   fun evaluatesDefaultPerObject() {
     val fory = ForyJsonKotlin.builder().withAsyncCompilation(false).build()
diff --git a/kotlin/fory-json-kotlin/src/test/kotlin/org/apache/fory/json/kotlin/KotlinNullabilityRuntimeTest.kt b/kotlin/fory-json-kotlin/src/test/kotlin/org/apache/fory/json/kotlin/KotlinNullabilityRuntimeTest.kt
index 89cf78b9c..a287e9a94 100644
--- a/kotlin/fory-json-kotlin/src/test/kotlin/org/apache/fory/json/kotlin/KotlinNullabilityRuntimeTest.kt
+++ b/kotlin/fory-json-kotlin/src/test/kotlin/org/apache/fory/json/kotlin/KotlinNullabilityRuntimeTest.kt
@@ -258,9 +258,10 @@ class KotlinNullabilityRuntimeTest {
         json.fromJson(explicitNull, type),
       )
 
-      assertFailsWith<ForyJsonException> {
-        json.fromJson("""{"required":"value","count":1,"nullableCount":null}""", type)
-      }
+      assertEquals(
+        ConstructorNulls("value", null, 1, null),
+        json.fromJson("""{"required":"value","count":1,"nullableCount":null}""", type),
+      )
       assertFailsWith<ForyJsonException> {
         json.fromJson(
           """{"required":null,"nullable":null,"count":1,"nullableCount":null}""",
@@ -416,8 +417,8 @@ class KotlinNullabilityRuntimeTest {
       val text = json.toJson(value, type)
       assertEquals("""{"id":1}""", text)
       assertEquals(text, json.toJsonBytes(value, type).decodeToString())
-      assertFailsWith<ForyJsonException> { json.fromJson(text, type) }
-      assertFailsWith<ForyJsonException> { json.fromJson(text.toByteArray(), type) }
+      assertEquals(value, json.fromJson(text, type))
+      assertEquals(value, json.fromJson(text.toByteArray(), type))
 
       val deferredType = jsonTypeRef<DeferredNullableModel>()
       val deferred = DeferredNullableModel(2).also { it.value = null }
diff --git a/scala/fory-json-scala/src/main/scala-2/org/apache/fory/json/scala/internal/ScalaTypeRefMacros.scala b/scala/fory-json-scala/src/main/scala-2/org/apache/fory/json/scala/internal/ScalaTypeRefMacros.scala
index 07ec36612..53a47e251 100644
--- a/scala/fory-json-scala/src/main/scala-2/org/apache/fory/json/scala/internal/ScalaTypeRefMacros.scala
+++ b/scala/fory-json-scala/src/main/scala-2/org/apache/fory/json/scala/internal/ScalaTypeRefMacros.scala
@@ -37,6 +37,11 @@ private[scala] object ScalaTypeRefMacros {
 
     def createTypeRef(tpe: Type): Tree = {
       val normalized = tpe.dealias
+      // Unit data uses BoxedUnit; classOf[Unit] is JVM void and selects the wrong codec,
+      // including when it occurs inside a generic argument or an array component.
+      if (normalized =:= definitions.UnitTpe) {
+        return q"_root_.org.apache.fory.reflect.TypeRef.of(classOf[_root_.scala.runtime.BoxedUnit])"
+      }
       // Value's JVM class is shared by every Enumeration. The stable prefix, when available,
       // belongs to this occurrence and must survive inside arrays and generic arguments.
       normalized match {
diff --git a/scala/fory-json-scala/src/main/scala-3/org/apache/fory/json/scala/internal/ScalaTypeRefMacros.scala b/scala/fory-json-scala/src/main/scala-3/org/apache/fory/json/scala/internal/ScalaTypeRefMacros.scala
index d53e60268..b0354d402 100644
--- a/scala/fory-json-scala/src/main/scala-3/org/apache/fory/json/scala/internal/ScalaTypeRefMacros.scala
+++ b/scala/fory-json-scala/src/main/scala-3/org/apache/fory/json/scala/internal/ScalaTypeRefMacros.scala
@@ -33,6 +33,11 @@ private[scala] object ScalaTypeRefMacros {
 
     def typeRefExpr(tpe: TypeRepr): Expr[org.apache.fory.reflect.TypeRef[?]] = {
       val normalized = tpe.dealias
+      // Unit data uses BoxedUnit; classOf[Unit] is JVM void and selects the wrong codec,
+      // including when it occurs inside a generic argument or an array component.
+      if (normalized =:= TypeRepr.of[Unit]) {
+        return '{ org.apache.fory.reflect.TypeRef.of(classOf[scala.runtime.BoxedUnit]) }
+      }
       val raw = normalized.classSymbol.getOrElse {
         report.errorAndAbort(s"${normalized.show} has no runtime class")
       }
diff --git a/scala/fory-json-scala/src/main/scala/org/apache/fory/json/scala/internal/ScalaCollectionCodecs.scala b/scala/fory-json-scala/src/main/scala/org/apache/fory/json/scala/internal/ScalaCollectionCodecs.scala
index dff171a83..679684687 100644
--- a/scala/fory-json-scala/src/main/scala/org/apache/fory/json/scala/internal/ScalaCollectionCodecs.scala
+++ b/scala/fory-json-scala/src/main/scala/org/apache/fory/json/scala/internal/ScalaCollectionCodecs.scala
@@ -540,11 +540,7 @@ private[scala] final class ScalaMapCodec(kind: Int, ownerBytes: Int, runtimeType
       runtimeType
     )
     val keyType = if (specializedKey == null) arguments(0) else specializedKey
-    val keyRawType = ScalaTypeSupport.rawType(keyType)
-    val enumerationKeyCodec = ScalaEnumerationTypes.mapKeyCodec(keyType)
-    keyCodec =
-      if (enumerationKeyCodec == null) resolver.getMapKeyCodec(keyRawType)
-      else enumerationKeyCodec
+    keyCodec = defaultKeyCodec(keyType, resolver)
     val valueType = arguments(if (specializedKey == null) 1 else 0)
     valueInfo = resolver.getTypeInfo(valueType, ScalaTypeSupport.rawType(valueType))
   }
@@ -566,11 +562,7 @@ private[scala] final class ScalaMapCodec(kind: Int, ownerBytes: Int, runtimeType
     keyCodec =
       if (childCodecs.keyCodec() != classOf[JsonCodec.NoMapKeyCodec])
         resolver.getMapKeyCodec(keyRawType, childCodecs.keyCodec())
-      else {
-        val enumerationKeyCodec = ScalaEnumerationTypes.mapKeyCodec(keyType)
-        if (enumerationKeyCodec == null) resolver.getMapKeyCodec(keyRawType)
-        else enumerationKeyCodec
-      }
+      else defaultKeyCodec(keyType, resolver)
     val valueType = arguments(if (specializedKey == null) 1 else 0)
     val valueRawType = ScalaTypeSupport.rawType(valueType)
     valueInfo =
@@ -579,6 +571,17 @@ private[scala] final class ScalaMapCodec(kind: Int, ownerBytes: Int, runtimeType
       else resolver.getTypeInfo(valueType, valueRawType, childCodecs.valueCodec())
   }
 
+  private def defaultKeyCodec(keyType: java.lang.reflect.Type, resolver: JsonTypeResolver): MapKeyCodec = {
+    val enumerationCodec = ScalaEnumerationTypes.mapKeyCodec(keyType)
+    if (enumerationCodec != null) return enumerationCodec
+    val rawType = ScalaTypeSupport.rawType(keyType)
+    val enumRoot = ScalaEnumCodec.enumRoot(rawType)
+    if (enumRoot != null && enumRoot == rawType) {
+      resolver.checkMapKeySecure(rawType)
+      ScalaEnumCodec.create(enumRoot, TypeRef.of(keyType))
+    } else resolver.getMapKeyCodec(rawType)
+  }
+
   override def writeString(writer: StringJsonWriter, value: scala.collection.Map[Any, Any]): Unit = {
     if (value == null) {
       writer.writeNull()
@@ -1237,6 +1240,31 @@ private[scala] object ScalaCollectionCodecs {
   val MutableAnyRefMapKind = 27
   val MutableLongMapKind = 28
 
+  def emptyValue(kind: Int, tag: ClassTag[Any]): AnyRef = kind match {
+    case ListKind => Nil
+    case VectorKind => Vector.empty
+    case ImmutableQueueKind => scala.collection.immutable.Queue.empty
+    case ImmutableArraySeqKind => scala.collection.immutable.ArraySeq.empty[Any](tag)
+    case MutableArrayBufferKind => scala.collection.mutable.ArrayBuffer.empty
+    case MutableListBufferKind => scala.collection.mutable.ListBuffer.empty
+    case MutableArraySeqKind => scala.collection.mutable.ArraySeq.make[Any](tag.newArray(0))
+    case MutableArrayDequeKind => scala.collection.mutable.ArrayDeque.empty
+    case MutableQueueKind => scala.collection.mutable.Queue.empty
+    case ImmutableHashSetKind => scala.collection.immutable.HashSet.empty
+    case ImmutableListSetKind => scala.collection.immutable.ListSet.empty
+    case MutableHashSetKind => scala.collection.mutable.HashSet.empty
+    case MutableLinkedHashSetKind => scala.collection.mutable.LinkedHashSet.empty
+    case ImmutableHashMapKind => scala.collection.immutable.HashMap.empty
+    case ImmutableVectorMapKind => scala.collection.immutable.VectorMap.empty
+    case ImmutableListMapKind => scala.collection.immutable.ListMap.empty
+    case ImmutableIntMapKind => scala.collection.immutable.IntMap.empty
+    case ImmutableLongMapKind => scala.collection.immutable.LongMap.empty
+    case MutableHashMapKind => scala.collection.mutable.HashMap.empty
+    case MutableLinkedHashMapKind => scala.collection.mutable.LinkedHashMap.empty
+    case MutableAnyRefMapKind => scala.collection.mutable.AnyRefMap.empty[AnyRef, Any]
+    case MutableLongMapKind => scala.collection.mutable.LongMap.empty
+  }
+
   def writeBooleanPair(writer: Utf8JsonWriter, first: Boolean, second: Boolean): Unit = {
     val firstBytes = if (first) 0x65_7572_742cL else 0x6573_6c61_662cL
     val secondBytes = if (second) 0x65_7572_742cL else 0x6573_6c61_662cL
@@ -1278,12 +1306,22 @@ private[scala] object ScalaCollectionCodecs {
     add(Vector(1))
     add(Vector.tabulate(33)(identity))
     add(scala.collection.immutable.Queue.empty)
+    add(scala.collection.immutable.Queue(1))
     add(scala.collection.immutable.ArraySeq.empty[Any])
     add(scala.collection.immutable.ArraySeq.empty[Int])
     add(scala.collection.mutable.ArrayBuffer.empty)
     add(scala.collection.mutable.ListBuffer.empty)
     add(scala.collection.mutable.ArraySeq.empty[Any])
-    add(scala.collection.mutable.ArraySeq.empty[Int])
+    // ArraySeq.empty ignores its ClassTag and always uses ofRef. Register each primitive
+    // backing class explicitly without admitting application subclasses of mutable.ArraySeq.
+    add(scala.collection.mutable.ArraySeq.make(Array.emptyBooleanArray))
+    add(scala.collection.mutable.ArraySeq.make(Array.emptyByteArray))
+    add(scala.collection.mutable.ArraySeq.make(Array.emptyShortArray))
+    add(scala.collection.mutable.ArraySeq.make(Array.emptyCharArray))
+    add(scala.collection.mutable.ArraySeq.make(Array.emptyIntArray))
+    add(scala.collection.mutable.ArraySeq.make(Array.emptyLongArray))
+    add(scala.collection.mutable.ArraySeq.make(Array.emptyFloatArray))
+    add(scala.collection.mutable.ArraySeq.make(Array.emptyDoubleArray))
     add(scala.collection.mutable.ArrayDeque.empty)
     add(scala.collection.mutable.Queue.empty)
     add(scala.collection.immutable.Set.empty)
diff --git a/scala/fory-json-scala/src/main/scala/org/apache/fory/json/scala/internal/ScalaEnumCodec.scala b/scala/fory-json-scala/src/main/scala/org/apache/fory/json/scala/internal/ScalaEnumCodec.scala
index 20332b38f..c2698eb10 100644
--- a/scala/fory-json-scala/src/main/scala/org/apache/fory/json/scala/internal/ScalaEnumCodec.scala
+++ b/scala/fory-json-scala/src/main/scala/org/apache/fory/json/scala/internal/ScalaEnumCodec.scala
@@ -24,7 +24,7 @@ import java.util.{HashMap, IdentityHashMap}
 
 import org.apache.fory.annotation.Internal
 import org.apache.fory.json.ForyJsonException
-import org.apache.fory.json.codec.{JsonValueCodec, StringEnumCodec}
+import org.apache.fory.json.codec.{MapKeyCodec, StringEnumCodec}
 import org.apache.fory.json.meta.JsonAsciiToken
 import org.apache.fory.json.reader.{Latin1JsonReader, Utf16JsonReader, Utf8JsonReader}
 import org.apache.fory.json.writer.StringJsonWriter
@@ -59,7 +59,7 @@ private[scala] object ScalaEnumCodec {
     null
   }
 
-  def create(typeClass: Class[_], typeRef: TypeRef[_]): JsonValueCodec[_] = {
+  def create(typeClass: Class[_], typeRef: TypeRef[_]): ScalaEnumCodec = {
     try {
       val valuesMethod = typeClass.getMethod("values")
       if (!Modifier.isPublic(valuesMethod.getModifiers) || !Modifier.isStatic(valuesMethod.getModifiers)) {
@@ -80,7 +80,7 @@ private[scala] object ScalaEnumCodec {
 class ScalaEnumCodec(
     typeClass: Class[_], values: Array[Object], names: Array[String]
 )
-    extends StringEnumCodec[Object](names) {
+    extends StringEnumCodec[Object](names) with MapKeyCodec {
   private val byName = new HashMap[String, Object](values.length * 2)
   private val indexByValue = new IdentityHashMap[Object, Integer](values.length * 2)
   private val tokenPrefixes = new Array[Long](values.length)
@@ -121,6 +121,10 @@ class ScalaEnumCodec(
     value
   }
 
+  override final def toName(key: Object): String = names(valueIndex(key))
+
+  override final def fromName(name: String): Object = value(name)
+
   override def writeString(writer: StringJsonWriter, v: Object): Unit = {
     if (v == null) writer.writeNull() else writer.writeString(names(valueIndex(v)))
   }
diff --git a/scala/fory-json-scala/src/main/scala/org/apache/fory/json/scala/internal/ScalaObjectModels.scala b/scala/fory-json-scala/src/main/scala/org/apache/fory/json/scala/internal/ScalaObjectModels.scala
index 1974ced6f..592c92485 100644
--- a/scala/fory-json-scala/src/main/scala/org/apache/fory/json/scala/internal/ScalaObjectModels.scala
+++ b/scala/fory-json-scala/src/main/scala/org/apache/fory/json/scala/internal/ScalaObjectModels.scala
@@ -20,13 +20,16 @@
 package org.apache.fory.json.scala.internal
 
 import java.lang.reflect.{Constructor, Field, Method, Modifier}
+import java.util.function.Supplier
 
 import org.apache.fory.json.ForyJsonException
 import org.apache.fory.json.codec.{JsonObjectModel, JsonValueCodec, ObjectCodec}
 import org.apache.fory.json.resolver.JsonTypeResolver
-import org.apache.fory.reflect.TypeRef
+import org.apache.fory.reflect.{ReflectionUtils, TypeRef}
 
 private[scala] object ScalaObjectModels {
+  private val optionDefault = classOf[Option[_]].getMethod("empty")
+
   def isCaseClass(typeClass: Class[_]): Boolean = {
     val name = typeClass.getName
     if (!classOf[Product].isAssignableFrom(typeClass) || name.startsWith("scala.Tuple")) {
@@ -39,16 +42,13 @@ private[scala] object ScalaObjectModels {
     // pick a constructor.
     try {
       val companion = companionOwner(typeClass, committed = false)
-      if (companion != null) findPrimaryConstructor(typeClass, companion) != null
+      if (companion != null && findPrimaryConstructor(typeClass, companion) != null) true
       else {
-        // A case class that cannot reach its companion, such as one declared inside a class or a
-        // method, is still a case class. Claim it so the codec reports the exact reason instead of
-        // leaving it to a generic object model that silently drops every property. A generated
+        // A case class with an unreachable companion or unsupported constructor still belongs
+        // here. Otherwise the generic object model can silently drop every property. A generated
         // `copy` returning the declaring class together with a declared `productPrefix`, which
         // `Product` otherwise supplies by default, is the compiler marker of a case class.
-        // Standard-library types keep their own mapping. A reachable companion whose constructor
-        // this module does not support, such as a varargs or non-public primary constructor, keeps
-        // its previous handling.
+        // Standard-library types keep their own mapping.
         !name.startsWith("scala.") && declaresCopy(typeClass) && declaresProductPrefix(typeClass)
       }
     } catch { case _: LinkageError => false }
@@ -117,6 +117,18 @@ private[scala] object ScalaObjectModels {
     val defaultsReceiver =
       if (companion.staticForwarders || defaults.forall(_ == null)) null
       else companionInstance(typeRef, companion)
+    // Missing properties use type defaults; explicit constructor defaults always take precedence.
+    // Factories run only for missing arguments and never share mutable empty values between objects.
+    val defaultFactories = new Array[Supplier[_]](defaults.length)
+    index = 0
+    while (index < defaults.length) {
+      if (defaults(index) == null && logicalParameterTypes(index).getRawType == classOf[Option[_]]) {
+        defaults(index) = optionDefault
+      } else if (defaults(index) == null) {
+        defaultFactories(index) = typeDefault(logicalParameterTypes(index), parameterTypes(index))
+      }
+      index += 1
+    }
     resolver.createObjectCodec(
       typeRef,
       new JsonObjectModel(
@@ -126,6 +138,7 @@ private[scala] object ScalaObjectModels {
         accessors,
         defaults,
         defaultsReceiver,
+        defaultFactories,
         Array.fill(names.length)(-1),
         Array.fill(names.length)(true),
         logicalParameterTypes,
@@ -137,6 +150,81 @@ private[scala] object ScalaObjectModels {
     )
   }
 
+  private def typeDefault(typeRef: TypeRef[_], carrier: Class[_]): Supplier[_] = {
+    val rawType = typeRef.getRawType
+    val collection = ScalaTypeCodecFactory.collectionDefault(typeRef)
+    if (collection != null) return collection
+    if (rawType.isArray) return () => java.lang.reflect.Array.newInstance(rawType.getComponentType, 0)
+    if (rawType == classOf[BigInt]) return () => BigInt(0)
+    if (rawType == classOf[BigDecimal]) return () => BigDecimal(0)
+    if (rawType == classOf[java.math.BigInteger]) return () => java.math.BigInteger.ZERO
+    if (rawType == classOf[java.math.BigDecimal]) return () => java.math.BigDecimal.ZERO
+    if (rawType == classOf[scala.runtime.BoxedUnit]) return () => scala.runtime.BoxedUnit.UNIT
+    if (rawType == classOf[java.util.EnumSet[_]]) {
+      val element = ScalaTypeSupport.rawType(ScalaTypeSupport.arguments(typeRef, 1, "EnumSet")(0))
+      return () => java.util.EnumSet.noneOf[Nothing](element.asInstanceOf[Class[Nothing]])
+    }
+    if (rawType == classOf[java.util.EnumMap[_, _]]) {
+      val key = ScalaTypeSupport.rawType(ScalaTypeSupport.arguments(typeRef, 2, "EnumMap")(0))
+      return () => new java.util.EnumMap[Nothing, Any](key.asInstanceOf[Class[Nothing]])
+    }
+    if (classOf[java.util.Map[_, _]].isAssignableFrom(rawType)) {
+      if (rawType.isAssignableFrom(classOf[java.util.LinkedHashMap[_, _]]))
+        return () => new java.util.LinkedHashMap[AnyRef, AnyRef]()
+      if (rawType.isAssignableFrom(classOf[java.util.TreeMap[_, _]]))
+        return () => new java.util.TreeMap[AnyRef, AnyRef]()
+      if (rawType.isAssignableFrom(classOf[java.util.concurrent.ConcurrentHashMap[_, _]]))
+        return () => new java.util.concurrent.ConcurrentHashMap[AnyRef, AnyRef]()
+      if (rawType.isAssignableFrom(classOf[java.util.concurrent.ConcurrentSkipListMap[_, _]]))
+        return () => new java.util.concurrent.ConcurrentSkipListMap[AnyRef, AnyRef]()
+    }
+    if (classOf[java.util.Collection[_]].isAssignableFrom(rawType)) {
+      if (rawType.isAssignableFrom(classOf[java.util.ArrayList[_]]))
+        return () => new java.util.ArrayList[AnyRef]()
+      if (rawType.isAssignableFrom(classOf[java.util.LinkedHashSet[_]]))
+        return () => new java.util.LinkedHashSet[AnyRef]()
+      if (rawType.isAssignableFrom(classOf[java.util.TreeSet[_]]))
+        return () => new java.util.TreeSet[AnyRef]()
+      if (rawType.isAssignableFrom(classOf[java.util.LinkedList[_]]))
+        return () => new java.util.LinkedList[AnyRef]()
+      if (rawType.isAssignableFrom(classOf[java.util.ArrayDeque[_]]))
+        return () => new java.util.ArrayDeque[AnyRef]()
+      if (rawType.isAssignableFrom(classOf[java.util.concurrent.LinkedBlockingDeque[_]]))
+        return () => new java.util.concurrent.LinkedBlockingDeque[AnyRef]()
+      if (rawType.isAssignableFrom(classOf[java.util.concurrent.LinkedBlockingQueue[_]]))
+        return () => new java.util.concurrent.LinkedBlockingQueue[AnyRef]()
+    }
+    if (
+      classOf[java.util.Collection[_]].isAssignableFrom(rawType) ||
+      classOf[java.util.Map[_, _]].isAssignableFrom(rawType)
+    ) {
+      // Only types with the ordinary collection constructor can use this construction rule.
+      // Custom collection codecs must remain usable even when their type has no such constructor.
+      if (rawType.getConstructors.exists(_.getParameterCount == 0)) {
+        val constructor = ReflectionUtils.getCtrHandle(rawType)
+        return () => constructor.invoke().asInstanceOf[AnyRef]
+      }
+    }
+    val primitive = if (carrier.isPrimitive) carrier else rawType
+    if (primitive == java.lang.Boolean.TYPE || primitive == classOf[java.lang.Boolean])
+      () => java.lang.Boolean.FALSE
+    else if (primitive == java.lang.Byte.TYPE || primitive == classOf[java.lang.Byte])
+      () => java.lang.Byte.valueOf(0.toByte)
+    else if (primitive == java.lang.Short.TYPE || primitive == classOf[java.lang.Short])
+      () => java.lang.Short.valueOf(0.toShort)
+    else if (primitive == java.lang.Integer.TYPE || primitive == classOf[java.lang.Integer])
+      () => java.lang.Integer.valueOf(0)
+    else if (primitive == java.lang.Long.TYPE || primitive == classOf[java.lang.Long])
+      () => java.lang.Long.valueOf(0L)
+    else if (primitive == java.lang.Float.TYPE || primitive == classOf[java.lang.Float])
+      () => java.lang.Float.valueOf(0F)
+    else if (primitive == java.lang.Double.TYPE || primitive == classOf[java.lang.Double])
+      () => java.lang.Double.valueOf(0D)
+    else if (primitive == java.lang.Character.TYPE || primitive == classOf[java.lang.Character])
+      () => java.lang.Character.valueOf(0.toChar)
+    else () => null
+  }
+
   def singletonCodec(
       typeRef: TypeRef[_],
       resolver: JsonTypeResolver
@@ -212,7 +300,8 @@ private[scala] object ScalaObjectModels {
   }
 
   private def declaresCopy(typeClass: Class[_]): Boolean = {
-    typeClass.getMethods.exists(method =>
+    // Scala 3 keeps copy private when the primary constructor is private.
+    typeClass.getDeclaredMethods.exists(method =>
       method.getName == "copy" && !Modifier.isStatic(method.getModifiers) &&
         !method.isBridge && !method.isSynthetic && method.getReturnType == typeClass
     )
diff --git a/scala/fory-json-scala/src/main/scala/org/apache/fory/json/scala/internal/ScalaScalarCodecs.scala b/scala/fory-json-scala/src/main/scala/org/apache/fory/json/scala/internal/ScalaScalarCodecs.scala
index 0447e6d04..298870274 100644
--- a/scala/fory-json-scala/src/main/scala/org/apache/fory/json/scala/internal/ScalaScalarCodecs.scala
+++ b/scala/fory-json-scala/src/main/scala/org/apache/fory/json/scala/internal/ScalaScalarCodecs.scala
@@ -199,7 +199,8 @@ private[scala] final class ScalaDurationCodec(finiteOnly: Boolean)
       case scala.concurrent.duration.Duration.MinusInf =>
         writer.writeFieldName("special")
         writer.writeString("MINUS_INF")
-      case scala.concurrent.duration.Duration.Undefined =>
+      // Undefined.equals is always false, so a stable-identifier pattern fails on Scala 2.
+      case undefined if undefined eq scala.concurrent.duration.Duration.Undefined =>
         writer.writeFieldName("special")
         writer.writeString("UNDEFINED")
       case _ => throw invalidDuration()
diff --git a/scala/fory-json-scala/src/main/scala/org/apache/fory/json/scala/internal/ScalaTupleCodec.scala b/scala/fory-json-scala/src/main/scala/org/apache/fory/json/scala/internal/ScalaTupleCodec.scala
index 8093409f1..ded8a56e9 100644
--- a/scala/fory-json-scala/src/main/scala/org/apache/fory/json/scala/internal/ScalaTupleCodec.scala
+++ b/scala/fory-json-scala/src/main/scala/org/apache/fory/json/scala/internal/ScalaTupleCodec.scala
@@ -445,7 +445,7 @@ private[scala] final class ScalaTupleCodec(arity: Int, tupleType: Class[_], runt
   }
 
   private def requireArity(value: Product): Unit = {
-    if (value.getClass != tupleType || value.productArity != arity) throw invalidArity()
+    if (!tupleType.isInstance(value) || value.productArity != arity) throw invalidArity()
   }
 
   private def invalidArity(): ForyJsonException =
@@ -456,6 +456,9 @@ private[scala] object ScalaTupleCodec {
   def arity(rawType: Class[_]): Int = {
     val name = rawType.getName
     if (!name.startsWith("scala.Tuple")) return -1
+    // Tuple1 and Tuple2 use specialized subclasses for primitive combinations. Their
+    // superclass owns the tuple schema; the specialization only changes the JVM carrier.
+    if (name.endsWith("$sp") && rawType.getSuperclass != null) return arity(rawType.getSuperclass)
     val suffix = name.substring("scala.Tuple".length)
     if (suffix.isEmpty || !suffix.forall(_.isDigit)) return -1
     val value = suffix.toInt
diff --git a/scala/fory-json-scala/src/main/scala/org/apache/fory/json/scala/internal/ScalaTypeCodecFactory.scala b/scala/fory-json-scala/src/main/scala/org/apache/fory/json/scala/internal/ScalaTypeCodecFactory.scala
index f393cd8ab..9f34fa167 100644
--- a/scala/fory-json-scala/src/main/scala/org/apache/fory/json/scala/internal/ScalaTypeCodecFactory.scala
+++ b/scala/fory-json-scala/src/main/scala/org/apache/fory/json/scala/internal/ScalaTypeCodecFactory.scala
@@ -20,6 +20,7 @@
 package org.apache.fory.json.scala.internal
 
 import java.lang.reflect.Modifier
+import java.util.function.Supplier
 
 import org.apache.fory.json.JsonCodecFactory
 import org.apache.fory.json.codec.JsonValueCodec
@@ -159,6 +160,26 @@ private[scala] object ScalaTypeCodecFactory extends JsonCodecFactory {
     }
   }
 
+  def collectionDefault(typeRef: TypeRef[_]): Supplier[_] = {
+    val rawType = typeRef.getRawType
+    if (rawType == classOf[List[_]] || rawType == Nil.getClass) return () => Nil
+    if (rawType == classOf[scala.collection.immutable.BitSet])
+      return () => scala.collection.immutable.BitSet.empty
+    if (rawType == classOf[scala.collection.mutable.BitSet])
+      return () => scala.collection.mutable.BitSet.empty
+    val selection =
+      if (classOf[scala.collection.Map[_, _]].isAssignableFrom(rawType)) mapKind(rawType, false)
+      else if (classOf[scala.collection.Iterable[_]].isAssignableFrom(rawType)) iterableKind(rawType, false)
+      else null
+    if (selection == null) return null
+    val kind = selection._1
+    val tag =
+      if (kind == ScalaCollectionCodecs.ImmutableArraySeqKind || kind == ScalaCollectionCodecs.MutableArraySeqKind)
+        ScalaTypeSupport.classTag(ScalaTypeSupport.rawType(ScalaTypeSupport.arguments(typeRef, 1, "Scala collection")(0)))
+      else null
+    () => ScalaCollectionCodecs.emptyValue(kind, tag)
+  }
+
   private def iterableKind(rawType: Class[_], runtimeWrite: Boolean): (Int, Class[_]) = {
     import ScalaCollectionCodecs._
 
diff --git a/scala/fory-json-scala/src/test/scala-3/org/apache/fory/json/scala/ScalaJsonDerivationSuite.scala b/scala/fory-json-scala/src/test/scala-3/org/apache/fory/json/scala/ScalaJsonDerivationSuite.scala
index 8009e387b..fcae5f728 100644
--- a/scala/fory-json-scala/src/test/scala-3/org/apache/fory/json/scala/ScalaJsonDerivationSuite.scala
+++ b/scala/fory-json-scala/src/test/scala-3/org/apache/fory/json/scala/ScalaJsonDerivationSuite.scala
@@ -79,6 +79,19 @@ enum Color {
   case Red, Blue
 }
 
+object Palette {
+  enum Shade {
+    case Light, Dark
+  }
+}
+
+case class EnumFields(name: String, color: Color, shade: Palette.Shade, colors: Map[Color, String])
+
+case class EnumMapValues(
+    @org.apache.fory.json.annotation.JsonCodec(valueCodec = classOf[TaggedStringCodec])
+    colors: Map[Color, String]
+)
+
 enum DisplayColor {
   case Red, Blue
 
@@ -268,11 +281,35 @@ class ScalaJsonDerivationSuite extends AnyFunSuite {
   }
 
   test("parameterless enum uses its declared root") {
-    val json = ForyJsonScala.builder().withCodegen(false).build()
-    assert(json.toJson(Color.Blue) == "\"Blue\"")
-    assert(json.fromJson("\"Blue\"", classOf[Color]) == Color.Blue)
-    assert(json.toJson(DisplayColor.Red) == "\"Red\"")
-    assert(json.fromJson("\"Blue\"", classOf[DisplayColor]) == DisplayColor.Blue)
+    for (json <- Seq(
+        ForyJsonScala.builder().withCodegen(false).build(),
+        ForyJsonScala.builder().withAsyncCompilation(false).build()
+      )) {
+      assert(json.toJson(Color.Blue) == "\"Blue\"")
+      assert(json.fromJson("\"Blue\"", classOf[Color]) == Color.Blue)
+      assert(json.toJson(DisplayColor.Red) == "\"Red\"")
+      assert(json.fromJson("\"Blue\"", classOf[DisplayColor]) == DisplayColor.Blue)
+      assertThrows[ForyJsonException](json.fromJson("\"Green\"", classOf[Color]))
+      assert(json.fromJson("\"Dark\"", classOf[Palette.Shade]) == Palette.Shade.Dark)
+      val value = EnumFields("中", Color.Red, Palette.Shade.Dark, Map(Color.Blue -> "蓝"))
+      assert(json.fromJson(json.toJson(value), classOf[EnumFields]) == value)
+      assert(json.fromJson(json.toJsonBytes(value), classOf[EnumFields]) == value)
+      val mapType = ScalaTypeRef[Map[DisplayColor, String]]
+      val map = Map(DisplayColor.Red -> "red")
+      assert(json.toJson(map, mapType) == """{"Red":"red"}""")
+      assert(json.fromJson("""{"Red":"red"}""", mapType) == map)
+      val tagged = EnumMapValues(Map(Color.Blue -> "blue"))
+      val taggedText = """{"colors":{"Blue":"tag:blue"}}"""
+      assert(json.toJson(tagged) == taggedText)
+      assert(json.fromJson(taggedText, classOf[EnumMapValues]) == tagged)
+      assert(json.fromJson(json.toJsonBytes(tagged), classOf[EnumMapValues]) == tagged)
+      assertThrows[ForyJsonException](json.fromJson(
+        """{"colors":{"Green":"unknown"}}""", classOf[EnumFields]))
+    }
+    val denied = ForyJsonScala.builder()
+      .withTypeChecker((name, _) => name != classOf[Color].getName).build()
+    assertThrows[org.apache.fory.exception.InsecureException](
+      denied.fromJson("{}", ScalaTypeRef[Map[Color, String]]))
   }
 
   test("derived sealed hierarchy uses inferred subtype names") {
diff --git a/scala/fory-json-scala/src/test/scala/org/apache/fory/json/scala/ScalaJsonSuite.scala b/scala/fory-json-scala/src/test/scala/org/apache/fory/json/scala/ScalaJsonSuite.scala
index 61cd125a8..2dd712816 100644
--- a/scala/fory-json-scala/src/test/scala/org/apache/fory/json/scala/ScalaJsonSuite.scala
+++ b/scala/fory-json-scala/src/test/scala/org/apache/fory/json/scala/ScalaJsonSuite.scala
@@ -70,6 +70,8 @@ case class BodyState(id: Int) {
 
 case class CurriedDefault(a: Int)(val b: Int = a + 1)
 
+case class MissingUnwrapped(@JsonUnwrapped point: NestedModels.Point)
+
 case class UnwrappedDetails(code: Int = 5) {
   var note: String = "default-note"
 }
@@ -88,6 +90,12 @@ object NestedModels {
 
   case class Span(from: Int)(val to: Int = from + 1)
 
+  case class Optional(value: Option[String], fallback: Option[String] = Some("default"))
+
+  case class OptionalOnly(value: Option[String])
+
+  case class OptionalDefault(value: Option[String])(val selected: String = value.getOrElse("default"))
+
   case class UnwrappedNested(code: Int = 5) {
     var note: String = "default-note"
   }
@@ -142,6 +150,8 @@ case class LongStringValues(
 
 case class UnitValue(value: Unit)
 
+case class UnitFields(value: Unit, option: Option[Unit], values: List[Unit], array: Array[Unit])
+
 case class ExplicitNullable(
     @JsonProperty(include = JsonProperty.Include.ALWAYS) value: String
 )
@@ -262,18 +272,67 @@ case class CodecSlots(
 
 case class Label(value: String) extends AnyVal
 
+case class TypedLabel[T](value: T) extends AnyVal
+
 case class Box[T](value: T)
 
+case class NamedBox[T](name: String, value: T)
+
 case class Boxes(ints: Box[Int], strings: Box[String])
 
 case class VarParams(var a: Int, var b: String = "b")
 
+case class PrivateConstructor private(value: Int)
+
+case class SecondaryConstructor(value: Int) {
+  def this(value: String) = this(value.toInt)
+}
+
+case class AmbiguousConstructor(value: Int) {
+  def this(value: String) = this(value.toInt)
+}
+
+object AmbiguousConstructor {
+  def apply(value: String): AmbiguousConstructor = new AmbiguousConstructor(value)
+}
+
+trait NamedValue {
+  val name: String
+}
+
+case class InheritedValue(name: String) extends NamedValue
+
+class CustomBuffer extends scala.collection.mutable.ArrayBuffer[Int]
+
+case class AlgebraicFields[T](
+    name: String,
+    queue: scala.collection.immutable.Queue[T],
+    arraySeq: scala.collection.mutable.ArraySeq[T],
+    pair: (T, T),
+    option: Option[T],
+    either: Either[String, T],
+    range: NumericRange[T]
+)
+
 case class Optionals(
     a: Option[Int],
     b: Option[String] = None,
     c: Option[Option[Int]] = None
 )
 
+case class OptionalFields(
+    value: Option[String],
+    fallback: Option[Int] = Some(7),
+    nested: Option[Option[Int]],
+    @JsonIgnore ignored: Option[String]
+)
+
+case class MissingValues(
+    byte: Byte, short: Short, int: Int, long: Long, float: Float, double: Double,
+    boolean: Boolean, char: Char, boxed: java.lang.Integer, big: BigInt, decimal: BigDecimal,
+    text: String, child: Node, @JsonIgnore ignored: Int, explicit: Int = 9
+)
+
 case class ScalarFields(
     decimal: BigDecimal,
     builder: scala.collection.mutable.StringBuilder,
@@ -524,20 +583,19 @@ class ScalaJsonSuite extends AnyFunSuite {
     assert(error.getMessage.contains("companion is not reachable"))
   }
 
-  test("required constructor values cannot be omitted as null") {
+  test("missing reference values use null") {
     for (json <- Seq(
         ForyJsonScala.builder().withCodegen(false).build(),
         ForyJsonScala.builder().withAsyncCompilation(false).build()
       )) {
-      assertThrows[org.apache.fory.json.ForyJsonException] {
-        json.toJson(NullableRequired(null))
-      }
+      assert(json.toJson(NullableRequired(null)) == "{}")
+      assert(json.fromJson("{}", classOf[NullableRequired]) == NullableRequired(null))
       assert(json.toJson(ExplicitNullable(null)) == "{\"value\":null}")
       assert(json.fromJson("{\"value\":null}", classOf[ExplicitNullable]) == ExplicitNullable(null))
     }
   }
 
-  test("required constructor values retain empty properties") {
+  test("constructor values follow empty inclusion") {
     for (codegen <- Seq(false, true)) {
       val json = ForyJsonScala.builder()
         .withCodegen(codegen)
@@ -546,13 +604,14 @@ class ScalaJsonSuite extends AnyFunSuite {
         .build()
       val value = EmptyRequired("", new java.util.ArrayList[String](), Array.emptyIntArray)
       val text = json.toJson(value)
-      assert(text == "{\"value\":\"\",\"items\":[],\"numbers\":[]}")
+      assert(text == "{}")
       assert(new String(json.toJsonBytes(value), UTF_8) == text)
       val decoded = json.fromJson(text, classOf[EmptyRequired])
-      assert(decoded.value == "")
+      assert(decoded.value == null)
       assert(decoded.items.isEmpty)
       assert(decoded.numbers.isEmpty)
-      assertThrows[ForyJsonException](json.toJson(ExplicitEmptyRequired("")))
+      assert(json.toJson(ExplicitEmptyRequired("")) == "{}")
+      assert(json.fromJson("{}", classOf[ExplicitEmptyRequired]) == ExplicitEmptyRequired(null))
       assert(json.toJson(EmptyDefault()) == "{}")
       assert(json.fromJson("{}", classOf[EmptyDefault]) == EmptyDefault())
     }
@@ -1418,6 +1477,8 @@ class ScalaJsonSuite extends AnyFunSuite {
     for (json <- runtimes) {
       assert(json.toJson(Duration.Inf, durationType) == """{"special":"INF"}""")
       assert(json.toJson(Duration.MinusInf, durationType) == """{"special":"MINUS_INF"}""")
+      assert(json.toJson(Duration.Undefined, durationType) == """{"special":"UNDEFINED"}""")
+      assert(new String(json.toJsonBytes(Duration.Undefined, durationType), UTF_8) == """{"special":"UNDEFINED"}""")
       assert(json.fromJson("""{"special":"INF"}""", durationType) eq Duration.Inf)
       assert(json.fromJson("""{"special":"MINUS_INF"}""".getBytes(UTF_8), durationType) eq Duration.MinusInf)
       assert(json.fromJson("""{"special":"UNDEFINED"}""", durationType) eq Duration.Undefined)
@@ -1433,6 +1494,11 @@ class ScalaJsonSuite extends AnyFunSuite {
       }
       val extreme = FiniteDuration(Long.MaxValue, TimeUnit.NANOSECONDS)
       assert(json.fromJson(json.toJson(extreme, finiteType), finiteType) == extreme)
+      val negative = FiniteDuration(-Long.MaxValue, TimeUnit.NANOSECONDS)
+      assert(json.fromJson(json.toJsonBytes(negative, finiteType), finiteType) == negative)
+      // FiniteDuration's supported range excludes Long.MinValue nanoseconds.
+      assertThrows[RuntimeException](json.fromJson(
+        """{"length":-9223372036854775808,"unit":"NANOSECONDS"}""", finiteType))
       assert(json.fromJson("null", finiteType) == null)
       assert(json.toJson(null.asInstanceOf[Duration], durationType) == "null")
 
@@ -1531,6 +1597,7 @@ class ScalaJsonSuite extends AnyFunSuite {
   test("declared collection kinds") {
     import scala.collection.{immutable => im, mutable => mu}
     val cases: Seq[(TypeRef[_ <: scala.collection.Iterable[Int]], Class[_])] = Seq(
+      (ScalaTypeRef[im.Queue[Int]], classOf[im.Queue[_]]),
       (ScalaTypeRef[mu.ArrayBuffer[Int]], classOf[mu.ArrayBuffer[_]]),
       (ScalaTypeRef[mu.ListBuffer[Int]], classOf[mu.ListBuffer[_]]),
       (ScalaTypeRef[mu.ArrayDeque[Int]], classOf[mu.ArrayDeque[_]]),
@@ -1554,6 +1621,8 @@ class ScalaJsonSuite extends AnyFunSuite {
       if (!decoded.isInstanceOf[scala.collection.Set[_]]) assert(decoded.toList == List(3, 1, 2))
       assert(json.fromJson("[]", anyRef).isEmpty)
       val encoded = json.toJson(decoded, anyRef)
+      assert(new String(json.toJsonBytes(decoded, anyRef), UTF_8) == encoded)
+      assert(json.toJson(decoded) == encoded)
       assert(json.fromJson(encoded.getBytes(UTF_8), anyRef) == decoded, s"$typeRef")
       assert(json.fromJson("null", anyRef) == null)
     }
@@ -1592,6 +1661,8 @@ class ScalaJsonSuite extends AnyFunSuite {
       }
       assert(json.fromJson("{}", anyRef).isEmpty)
       val encoded = json.toJson(decoded, anyRef)
+      assert(new String(json.toJsonBytes(decoded, anyRef), UTF_8) == encoded)
+      assert(json.toJson(decoded) == encoded)
       assert(json.fromJson(encoded.getBytes(UTF_8), anyRef) == decoded, s"$typeRef")
       assert(json.fromJson("null", anyRef) == null)
       assertThrows[ForyJsonException](json.fromJson("[1]", anyRef))
@@ -1610,9 +1681,33 @@ class ScalaJsonSuite extends AnyFunSuite {
     assert(roundTrip(json, List("a", null, "b"), nullableList) == List("a", null, "b"))
   }
 
+  test("primitive mutable ArraySeq") {
+    import scala.collection.mutable.ArraySeq
+    val cases: Seq[(AnyRef, TypeRef[_], String)] = Seq(
+      (ArraySeq[Boolean](true, false), ScalaTypeRef[ArraySeq[Boolean]], "[true,false]"),
+      (ArraySeq[Byte](1, 2), ScalaTypeRef[ArraySeq[Byte]], "[1,2]"),
+      (ArraySeq[Short](1, 2), ScalaTypeRef[ArraySeq[Short]], "[1,2]"),
+      (ArraySeq[Char]('a', '中'), ScalaTypeRef[ArraySeq[Char]], """["a","中"]"""),
+      (ArraySeq[Int](1, 2), ScalaTypeRef[ArraySeq[Int]], "[1,2]"),
+      (ArraySeq[Long](1, 2), ScalaTypeRef[ArraySeq[Long]], "[1,2]"),
+      (ArraySeq[Float](1.5f), ScalaTypeRef[ArraySeq[Float]], "[1.5]"),
+      (ArraySeq[Double](1.5), ScalaTypeRef[ArraySeq[Double]], "[1.5]"),
+      (ArraySeq[String]("a"), ScalaTypeRef[ArraySeq[String]], """["a"]""")
+    )
+    for (json <- runtimes; (value, declared, expected) <- cases) {
+      val typeRef = declared.asInstanceOf[TypeRef[AnyRef]]
+      assert(roundTrip(json, value, typeRef) == value)
+      assert(json.toJson(value, typeRef) == expected)
+      assert(json.toJson(value) == expected)
+      assert(new String(json.toJsonBytes(value), UTF_8) == expected)
+    }
+  }
+
   test("runtime collection writes and unsupported families") {
     import scala.collection.{immutable => im, mutable => mu}
     val json = runtimes.head
+    assert(json.toJson(im.Queue(1, 2)) == "[1,2]")
+    assert(new String(json.toJsonBytes(im.Queue(1, 2)), UTF_8) == "[1,2]")
     assert(json.toJson(mu.ArrayBuffer(1, 2)) == "[1,2]")
     assert(json.toJson(mu.ListBuffer(1, 2)) == "[1,2]")
     assert(json.toJson(mu.ArrayDeque(1, 2)) == "[1,2]")
@@ -1629,6 +1724,10 @@ class ScalaJsonSuite extends AnyFunSuite {
     assertThrows[ForyJsonException](json.toJson(im.TreeSet(1, 2)))
     assertThrows[ForyJsonException](json.toJson(im.TreeMap("a" -> 1)))
     assertThrows[ForyJsonException](json.toJson(mu.TreeSet(1, 2)))
+    val custom = new CustomBuffer
+    custom += 1
+    assertThrows[ForyJsonException](json.toJson(custom))
+    assertThrows[ForyJsonException](json.toJson(custom, ScalaTypeRef[mu.ArrayBuffer[Int]]))
     assertThrows[ForyJsonException](json.fromJson("[1]", ScalaTypeRef[im.TreeSet[Int]]))
     assertThrows[ForyJsonException](json.fromJson("""{"a":1}""", ScalaTypeRef[im.SortedMap[String, Int]]))
     assertThrows[ForyJsonException](json.fromJson("[1]", ScalaTypeRef[im.SortedSet[Int]]))
@@ -1675,6 +1774,9 @@ class ScalaJsonSuite extends AnyFunSuite {
         json.fromJson("null", typeRef.asInstanceOf[TypeRef[AnyRef]])
       }
       assert(error.getMessage.contains("runtime-state or lazy type"), s"$typeRef: ${error.getMessage}")
+      val fieldType = TypeRef.ofDeclaredTypeArguments(
+        classOf[Box[_]], null, java.util.Collections.singletonList(typeRef), null)
+      assertThrows[UnsupportedJsonTypeException](json.fromJson("{}", fieldType))
     }
     assertThrows[UnsupportedJsonTypeException](json.toJson(scala.util.Success(1)))
     assertThrows[UnsupportedJsonTypeException](json.toJson(Symbol("x")))
@@ -1691,7 +1793,8 @@ class ScalaJsonSuite extends AnyFunSuite {
       assert(json.toJson(Optionals(Some(1))) == """{"a":1,"b":null,"c":null}""")
       assert(json.fromJson("""{"a":1}""", classOf[Optionals]) == Optionals(Some(1)))
       assert(json.fromJson("""{"a":null}""", classOf[Optionals]) == Optionals(None))
-      assertThrows[ForyJsonException](json.fromJson("{}", classOf[Optionals]))
+      assert(json.fromJson("{}", classOf[Optionals]) == Optionals(None))
+      assert(json.fromJson("{}".getBytes(UTF_8), classOf[Optionals]) == Optionals(None))
       assert(
         json.fromJson("""{"a":1,"b":"x","c":2}""", classOf[Optionals]) ==
           Optionals(Some(1), Some("x"), Some(Some(2)))
@@ -1708,6 +1811,97 @@ class ScalaJsonSuite extends AnyFunSuite {
     }
   }
 
+  test("Option constructor defaults") {
+    for (json <- runtimes) {
+      val empty = OptionalFields(None, Some(7), None, None)
+      for (text <- Seq("{}", """{"unknown":"中"}""")) {
+        assert(json.fromJson(text, classOf[OptionalFields]) == empty)
+        assert(json.fromJson(text.getBytes(UTF_8), classOf[OptionalFields]) == empty)
+        assert(json.fromJson(text, classOf[NestedModels.Optional]) == NestedModels.Optional(None))
+        assert(
+          json.fromJson(text.getBytes(UTF_8), classOf[NestedModels.Optional]) ==
+            NestedModels.Optional(None))
+        assert(
+          json.fromJson(text, classOf[NestedModels.OptionalOnly]) == NestedModels.OptionalOnly(None))
+        assert(json.fromJson(text, classOf[NestedModels.OptionalDefault]).selected == "default")
+      }
+      val present = """{"value":"中","fallback":null,"nested":2}"""
+      val expected = OptionalFields(Some("中"), None, Some(Some(2)), None)
+      assert(json.fromJson(present, classOf[OptionalFields]) == expected)
+      assert(json.fromJson(present.getBytes(UTF_8), classOf[OptionalFields]) == expected)
+      assert(
+        json.fromJson("""{"value":"present"}""", classOf[NestedModels.OptionalDefault]).selected ==
+          "present")
+      val boxType = ScalaTypeRef[Box[Option[String]]]
+      assert(json.fromJson("{}", boxType) == Box(None))
+      assert(json.fromJson("{}".getBytes(UTF_8), boxType) == Box(None))
+      assert(json.fromJson("""{"value":1}""", classOf[Node]) == Node(1, None))
+      assert(json.fromJson("{}", classOf[Node]) == Node(0, None))
+      assert(json.fromJson(json.toJson(empty), classOf[OptionalFields]) == empty)
+    }
+  }
+
+  test("missing constructor values use type defaults") {
+    for (json <- runtimes; text <- Seq("{}", """{"unknown":"中"}""")) {
+      val expected = MissingValues(0, 0, 0, 0L, 0F, 0D, false, 0.toChar, 0, BigInt(0), BigDecimal(0), null, null, 0)
+      assert(json.fromJson(text, classOf[MissingValues]) == expected)
+      assert(json.fromJson(text.getBytes(UTF_8), classOf[MissingValues]) == expected)
+      assert(json.fromJson(text, classOf[CurriedDefault]).b == 1)
+      assert(json.fromJson(text, classOf[NestedModels.Span]).to == 1)
+      assert(json.fromJson(text, classOf[BodyState]).id == 0)
+      val numbers = json.fromJson(text, ScalaTypeRef[Box[Array[Int]]]).value
+      assert(numbers.isEmpty)
+      val objects = json.fromJson(text, ScalaTypeRef[Box[Array[Node]]]).value
+      assert(objects.isEmpty)
+      assert(json.fromJson(text, classOf[MissingUnwrapped]) == MissingUnwrapped(null))
+      val flattened = """{"x":2,"unknown":"中"}"""
+      val point = MissingUnwrapped(NestedModels.Point(2, null))
+      assert(json.fromJson(flattened, classOf[MissingUnwrapped]) == point)
+      assert(json.fromJson(flattened.getBytes(UTF_8), classOf[MissingUnwrapped]) == point)
+    }
+  }
+
+  test("missing collections use independent empty values") {
+    import scala.collection.{immutable => im, mutable => mu}
+    val types = Seq[TypeRef[_]](
+      ScalaTypeRef[Box[List[String]]], ScalaTypeRef[Box[Vector[String]]],
+      ScalaTypeRef[Box[Seq[String]]], ScalaTypeRef[Box[im.Queue[String]]],
+      ScalaTypeRef[Box[im.ArraySeq[String]]], ScalaTypeRef[Box[im.HashSet[String]]],
+      ScalaTypeRef[Box[im.ListSet[String]]], ScalaTypeRef[Box[im.BitSet]],
+      ScalaTypeRef[Box[mu.ArrayBuffer[String]]], ScalaTypeRef[Box[mu.ListBuffer[String]]],
+      ScalaTypeRef[Box[mu.ArraySeq[String]]], ScalaTypeRef[Box[mu.ArrayDeque[String]]],
+      ScalaTypeRef[Box[mu.Queue[String]]], ScalaTypeRef[Box[mu.HashSet[String]]],
+      ScalaTypeRef[Box[mu.LinkedHashSet[String]]], ScalaTypeRef[Box[mu.BitSet]],
+      ScalaTypeRef[Box[Map[String, String]]], ScalaTypeRef[Box[im.VectorMap[String, String]]],
+      ScalaTypeRef[Box[im.ListMap[String, String]]], ScalaTypeRef[Box[im.IntMap[String]]],
+      ScalaTypeRef[Box[im.LongMap[String]]], ScalaTypeRef[Box[mu.HashMap[String, String]]],
+      ScalaTypeRef[Box[mu.LinkedHashMap[String, String]]],
+      ScalaTypeRef[Box[mu.AnyRefMap[String, String]]], ScalaTypeRef[Box[mu.LongMap[String]]]
+    )
+    for (json <- runtimes) {
+      for (kind <- types) {
+        val first = json.fromJson("{}", kind).asInstanceOf[Box[scala.collection.Iterable[_]]].value
+        val second = json.fromJson("{}".getBytes(UTF_8), kind).asInstanceOf[Box[scala.collection.Iterable[_]]].value
+        assert(first.isEmpty && second.isEmpty)
+        if (first.isInstanceOf[mu.Iterable[_]]) assert(first ne second)
+      }
+      val kind = ScalaTypeRef[Box[mu.ArrayBuffer[String]]]
+      val first = json.fromJson("{}", kind).value
+      first += "changed"
+      assert(json.fromJson("{}", kind).value.isEmpty)
+      val queue = json.fromJson("{}", ScalaTypeRef[Box[java.util.PriorityQueue[String]]]).value
+      assert(queue.isEmpty)
+      val mapType = ScalaTypeRef[Box[java.util.concurrent.ConcurrentMap[String, String]]]
+      val map = json.fromJson("{}", mapType).value
+      map.put("key", "changed")
+      assert(json.fromJson("{}", mapType).value.isEmpty)
+      val enumSet = ScalaTypeRef[Box[java.util.EnumSet[java.util.concurrent.TimeUnit]]]
+      val enumMap = ScalaTypeRef[Box[java.util.EnumMap[java.util.concurrent.TimeUnit, String]]]
+      assert(json.fromJson("{}", enumSet).value.isEmpty)
+      assert(json.fromJson("{}", enumMap).value.isEmpty)
+    }
+  }
+
   test("tuple shapes") {
     for (json <- runtimes) {
       val tuple1Type = ScalaTypeRef[Tuple1[String]]
@@ -1752,17 +1946,121 @@ class ScalaJsonSuite extends AnyFunSuite {
       assert(roundTrip(json, Map("k" -> Label("v")), mapType) == Map("k" -> Label("v")))
       val optionType = ScalaTypeRef[Option[Label]]
       assert(roundTrip(json, Some(Label("v")): Option[Label], optionType) == Some(Label("v")))
+      val genericType = ScalaTypeRef[TypedLabel[String]]
+      assert(roundTrip(json, TypedLabel("中"), genericType) == TypedLabel("中"))
+      assertThrows[UnsupportedJsonTypeException](json.fromJson("null", classOf[TypedLabel[_]]))
+    }
+  }
+
+  test("specialized tuples") {
+    val cases: Seq[(Product, TypeRef[_], String)] = Seq(
+      (Tuple1(1), ScalaTypeRef[Tuple1[Int]], "[1]"),
+      ((1, 2), ScalaTypeRef[(Int, Int)], "[1,2]"),
+      ((1L, 2.5), ScalaTypeRef[(Long, Double)], "[1,2.5]"),
+      ((true, '中'), ScalaTypeRef[(Boolean, Char)], """[true,"中"]""")
+    )
+    for (json <- runtimes; (value, declared, expected) <- cases) {
+      val typeRef = declared.asInstanceOf[TypeRef[Product]]
+      assert(roundTrip(json, value, typeRef) == value)
+      assert(json.toJson(value, typeRef) == expected)
+      assert(json.toJson(value) == expected)
+      assert(new String(json.toJsonBytes(value), UTF_8) == expected)
     }
   }
 
-  test("Unit and singletons") {
+  test("primitive generic case classes") {
+    val cases: Seq[(Any, TypeRef[_], TypeRef[_])] = Seq(
+      (true, ScalaTypeRef[Box[Boolean]], ScalaTypeRef[NamedBox[Boolean]]),
+      (1.toByte, ScalaTypeRef[Box[Byte]], ScalaTypeRef[NamedBox[Byte]]),
+      (2.toShort, ScalaTypeRef[Box[Short]], ScalaTypeRef[NamedBox[Short]]),
+      ('中', ScalaTypeRef[Box[Char]], ScalaTypeRef[NamedBox[Char]]),
+      (3, ScalaTypeRef[Box[Int]], ScalaTypeRef[NamedBox[Int]]),
+      (4L, ScalaTypeRef[Box[Long]], ScalaTypeRef[NamedBox[Long]]),
+      (1.5f, ScalaTypeRef[Box[Float]], ScalaTypeRef[NamedBox[Float]]),
+      (2.5, ScalaTypeRef[Box[Double]], ScalaTypeRef[NamedBox[Double]])
+    )
+    for (json <- runtimes; (value, boxType, namedType) <- cases) {
+      assert(roundTrip(json, Box(value), boxType.asInstanceOf[TypeRef[Box[Any]]]) == Box(value))
+      val named = NamedBox("中", value)
+      assert(roundTrip(json, named, namedType.asInstanceOf[TypeRef[NamedBox[Any]]]) == named)
+    }
+  }
+
+  test("nested generic case classes") {
+    val typeRef = ScalaTypeRef[Box[Box[Option[Int]]]]
+    val value = Box(Box(Some(1): Option[Int]))
     for (json <- runtimes) {
-      val unitType = classOf[scala.runtime.BoxedUnit]
-      assert(json.toJson(scala.runtime.BoxedUnit.UNIT, unitType) == "null")
-      assert(json.fromJson("null", unitType) != null)
+      assert(roundTrip(json, value, typeRef) == value)
+      assert(json.toJson(value, typeRef) == """{"value":{"value":1}}""")
+    }
+  }
+
+  test("Unit type tokens") {
+    type Completion = Unit
+    val unitType = ScalaTypeRef[Completion]
+    assert(unitType.getRawType == classOf[scala.runtime.BoxedUnit])
+    for (json <- runtimes) {
+      assert(json.toJson((), unitType) == "null")
+      assert(new String(json.toJsonBytes((), unitType), UTF_8) == "null")
+      json.fromJson("null", unitType)
+      val boxedType = unitType.asInstanceOf[TypeRef[AnyRef]]
+      assert(json.fromJson("null", boxedType) eq scala.runtime.BoxedUnit.UNIT)
+      assert(json.fromJson("null".getBytes(UTF_8), boxedType) eq scala.runtime.BoxedUnit.UNIT)
       assertThrows[ForyJsonException](json.fromJson("1", unitType))
+      assertThrows[IllegalArgumentException](json.toJson((), classOf[Unit]))
+    }
+  }
+
+  test("Unit in parameterized types") {
+    val optionType = ScalaTypeRef[Option[Unit]]
+    val listType = ScalaTypeRef[List[Unit]]
+    val arrayType = ScalaTypeRef[Array[Unit]]
+    val boxType = ScalaTypeRef[Box[Unit]]
+    val nestedType = ScalaTypeRef[List[Option[Unit]]]
+    for (json <- runtimes) {
+      assert(json.toJson(Some(()): Option[Unit], optionType) == "null")
+      assert(json.fromJson("null", optionType) == None)
+      assertThrows[ForyJsonException](json.fromJson("1", optionType))
+      val values = roundTrip(json, List((), ()), listType)
+      assert(values.asInstanceOf[List[AnyRef]].forall(_ eq scala.runtime.BoxedUnit.UNIT))
+      assert(json.toJson(values, listType) == "[null,null]")
+      assert(json.toJson(Array((), ()), arrayType) == "[null,null]")
+      assert(new String(json.toJsonBytes(Array((), ()), arrayType), UTF_8) == "[null,null]")
+      for (array <- Seq(
+          json.fromJson("[null,null]", arrayType),
+          json.fromJson("[null,null]".getBytes(UTF_8), arrayType)
+        )) {
+        assert(array.length == 2)
+        assert(array.asInstanceOf[Array[AnyRef]].forall(_ eq scala.runtime.BoxedUnit.UNIT))
+      }
+      val box = roundTrip(json, Box(()), boxType)
+      assert(box.asInstanceOf[Box[AnyRef]].value eq scala.runtime.BoxedUnit.UNIT)
+      assert(roundTrip(json, List(Some(()), None), nestedType) == List(None, None))
+    }
+  }
+
+  test("Unit case-class fields") {
+    for (json <- runtimes) {
+      val value = UnitFields((), Some(()), List(()), Array(()))
+      val text = """{"option":null,"values":[null],"array":[null],"value":null}"""
+      assert(json.toJson(value) == text)
+      assert(new String(json.toJsonBytes(value), UTF_8) == text)
+      for (decoded <- Seq(
+          json.fromJson(text, classOf[UnitFields]),
+          json.fromJson(text.getBytes(UTF_8), classOf[UnitFields])
+        )) {
+        assert(decoded.productElement(0) == (()))
+        assert(decoded.option == None)
+        assert(decoded.values.asInstanceOf[List[AnyRef]].head eq scala.runtime.BoxedUnit.UNIT)
+        assert(decoded.array.asInstanceOf[Array[AnyRef]].head eq scala.runtime.BoxedUnit.UNIT)
+      }
+      assert(json.fromJson("{}", classOf[UnitValue]) == UnitValue(()))
       assertThrows[ForyJsonException](json.fromJson("""{"value":1}""", classOf[UnitValue]))
+    }
+  }
 
+  test("singletons") {
+    for (json <- runtimes) {
       assert(json.toJson(Marker) == "{}")
       assert(json.fromJson("{}", Marker.getClass) eq Marker)
       assert(json.fromJson("{}".getBytes(UTF_8), Marker.getClass) eq Marker)
@@ -1799,8 +2097,8 @@ class ScalaJsonSuite extends AnyFunSuite {
       assert(json.fromJson(text.getBytes(UTF_8), classOf[Wide]) == wide)
       val withoutDefault = text.replace(""","f24":24""", "")
       assert(json.fromJson(withoutDefault, classOf[Wide]) == wide)
-      val missing = intercept[ForyJsonException](json.fromJson("""{"f1":1}""", classOf[Wide]))
-      assert(missing.getMessage != null)
+      val missing = json.fromJson("""{"f1":1}""", classOf[Wide])
+      assert(missing.f1 == 1 && missing.f2 == 0 && missing.f23 == 0 && missing.f24 == 24)
 
       // Unknown properties are skipped for case classes like other object codecs.
       assert(json.fromJson("""{"a":2,"zzz":{"deep":[1,2]}}""", classOf[VarParams]) == VarParams(2))
@@ -1827,6 +2125,38 @@ class ScalaJsonSuite extends AnyFunSuite {
     }
   }
 
+  test("case class constructor shapes") {
+    for (json <- runtimes) {
+      // Scala 2 exposes this private constructor and its apply as public bytecode; Scala 3
+      // hides apply, so JSON cannot identify a supported public primary constructor there.
+      if (classOf[PrivateConstructor].getMethods.exists(_.getName == "apply")) {
+        assert(json.fromJson("""{"value":1}""", classOf[PrivateConstructor]).value == 1)
+      } else {
+        assertThrows[ForyJsonException](json.fromJson("""{"value":1}""", classOf[PrivateConstructor]))
+      }
+      assertThrows[ForyJsonException](json.fromJson("""{"value":1}""", classOf[AmbiguousConstructor]))
+      val secondary = new SecondaryConstructor("2")
+      assert(json.fromJson(json.toJson(secondary), classOf[SecondaryConstructor]) == secondary)
+      val inherited = InheritedValue("中")
+      assert(json.fromJson(json.toJsonBytes(inherited), classOf[InheritedValue]) == inherited)
+      assert(json.fromJson("""{"value":1}""", classOf[Box[_]]).value == 1L)
+    }
+    val bounded = ForyJsonScala.builder().maxDepth(2).build()
+    val nested = """{"value":1,"next":{"value":2,"next":{"value":3,"next":null}}}"""
+    assertThrows[ForyJsonException](bounded.fromJson(nested, classOf[Node]))
+    assert(bounded.fromJson("""{"value":4,"next":null}""", classOf[Node]) == Node(4, None))
+  }
+
+  test("algebraic fields with codegen") {
+    val typeRef = ScalaTypeRef[AlgebraicFields[Int]]
+    val value = AlgebraicFields(
+      "中", scala.collection.immutable.Queue(1, 2), scala.collection.mutable.ArraySeq(3, 4),
+      (5, 6), Some(7), Right(8), NumericRange.inclusive(1, 3, 1))
+    for (json <- runtimes) {
+      assert(roundTrip(json, value, typeRef) == value)
+    }
+  }
+
 
   test("Enumeration codec registered directly") {
     val codec = new HueCodec


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