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/23 01:34:03 UTC

(fory) branch main updated: fix(json): retain properties without defaults under NON_DEFAULT (#4078)

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 d15f7af10 fix(json): retain properties without defaults under NON_DEFAULT (#4078)
d15f7af10 is described below

commit d15f7af104521a11509362a2d0e151c1163698ad
Author: Shawn Yang <sh...@gmail.com>
AuthorDate: Wed Sep 23 09:33:58 2026 +0800

    fix(json): retain properties without defaults under NON_DEFAULT (#4078)
    
    ## Why?
    
    `NON_DEFAULT` rejected properties without defaults, so a Scala model
    such as `@JsonInclude(NON_DEFAULT) case class Request(id: String,
    retries: Int = 3)` could not serialize without an `ALWAYS` override on
    `id`. Java parameterized creators and Kotlin required parameters had the
    same problem.
    
    ## What does this PR do?
    
    Keep properties without defaults in the output under both class-level
    `@JsonInclude(NON_DEFAULT)` and field-level `@JsonProperty(include =
    NON_DEFAULT)`, including null, zero, false and empty values. Apply the
    decision in the existing codec initialization path; per-write omission
    and generated code remain unchanged.
    
    - Scala keeps properties without a supported compiler default method and
    continues comparing declared defaults using the current object's actual
    dependencies.
    - Java keeps authorized properties when the selected read path has no
    no-argument reference constructor. An unrelated no-argument constructor
    does not replace the selected creator.
    - Kotlin keeps required constructor properties and uninitialized
    `lateinit` properties in the write schema. Defaulted properties still
    require a legal all-default reference construction path; required
    arguments are never fabricated.
    - Preserve failures from selected default methods, unavailable
    dependencies, and reference construction. Keep global `NON_DEFAULT`
    rejection.
    - Update annotation Javadocs and JSON guides, and cover Mixin and
    generated/non-generated paths.
    
    ## Validation
    
    OpenJDK 25.0.3:
    
    - Java: `JsonInclusionTest` (30 tests) and `JsonTypeProcessorTest` (73
    tests), including actual compilation and use of generated codecs.
    - Scala: `ScalaJsonSuite`, 78 tests each on Scala 2.13.18 and 3.3.8.
    - Kotlin: `KotlinDefaultArgumentsTest` (4 tests), including String/UTF-8
    and compact/pretty output with codegen on/off.
    - Kotlin KSP: `R8RulesWriterTest` and `ProcessorResourceTest` (10
    tests). Existing retention paths need no production changes. These
    resource tests are not Android/native-image end-to-end validation.
    - Java Spotless/Checkstyle, Kotlin Spotless, Markdown formatting and
    `git diff --check` passed.
    
    No GraalVM or benchmarks were run.
    
    ## Does this PR introduce any user-facing change?
    
    Properties without defaults no longer cause an error under class-level
    or field-level `NON_DEFAULT`; their values are serialized. No public API
    additions or binary protocol changes.
---
 .agents/languages/kotlin.md                        |  4 +-
 .agents/languages/scala.md                         |  3 +-
 docs/json/annotations.md                           | 27 ++++++----
 docs/json/kotlin.md                                | 14 +++--
 docs/json/object-mapping.md                        |  3 +-
 docs/json/scala.md                                 | 18 ++++---
 .../processing/JsonTypeProcessorTest.java          | 63 ++++++++++++++++++++++
 .../apache/fory/json/annotation/JsonInclude.java   | 10 +++-
 .../apache/fory/json/annotation/JsonProperty.java  |  8 ++-
 .../apache/fory/json/codec/ObjectCodecBuilder.java | 23 +++++++-
 .../org/apache/fory/json/JsonInclusionTest.java    | 51 ++++++++++++++++--
 .../fory/json/kotlin/KotlinDefaultArgumentsTest.kt | 52 +++++++++++++++++-
 .../apache/fory/json/scala/ScalaJsonSuite.scala    | 45 ++++++++++++----
 13 files changed, 278 insertions(+), 43 deletions(-)

diff --git a/.agents/languages/kotlin.md b/.agents/languages/kotlin.md
index 769e58070..ca7b5dff8 100644
--- a/.agents/languages/kotlin.md
+++ b/.agents/languages/kotlin.md
@@ -12,7 +12,9 @@ Load this file when changing `kotlin/` or compiler code that generates Kotlin so
   `NON_NULL` or `NON_EMPTY` to guarantee round trips. Missing-property defaults and nullability
   remain reader-owned.
 - `NON_DEFAULT` requires explicit property or class authorization; global use is invalid. Kotlin
-  defaults use one reference object per model metadata only when the selected constructor has no
+  properties without defaults, including required parameters and lateinit properties, remain written
+  under either authorization form. A defaulted property still requires a legal reference baseline.
+  Kotlin defaults use one reference object per model metadata only when the selected constructor has no
   required parameters. Never fabricate required arguments, change creator selection, or construct
   a comparison object on each write. Authorization covers constructor and initializer execution;
   the caller owns stable defaults and consistent missing-field recovery, including dependencies.
diff --git a/.agents/languages/scala.md b/.agents/languages/scala.md
index 13caf1413..2df812237 100644
--- a/.agents/languages/scala.md
+++ b/.agents/languages/scala.md
@@ -26,7 +26,8 @@ Load this file when changing `scala/`.
   default methods are omission sources; reader type fallbacks must stay distinct. Evaluate dependent
   defaults with actual preceding object properties and reject missing dependencies. Do not infer
   purity from method availability or use a default reference object for parameter-dependent Scala
-  defaults. Missing-field recovery remains reader-owned.
+  defaults. Both class-level and field-level NON_DEFAULT retain properties without a supported
+  compiler default method. Missing-field recovery remains reader-owned.
 
 ## Commands
 
diff --git a/docs/json/annotations.md b/docs/json/annotations.md
index be2adefc8..916ccc875 100644
--- a/docs/json/annotations.md
+++ b/docs/json/annotations.md
@@ -163,8 +163,9 @@ The supported inclusion values are:
   JDK `Optional`, `OptionalInt`, `OptionalLong`, and `OptionalDouble` values.
   With the Scala module, also omit `None` and supported empty strict Scala collections and maps.
 - `NON_DEFAULT`: explicitly authorize omission when the property equals a supported default.
-  Different values, including null or empty values, remain included. Unsupported default sources
-  cause an error when the model is initialized. See [Default omission](#jsoninclude-and-default-omission).
+  Different values, including null or empty values, remain included. Properties without defaults
+  remain included under both field-level and class-level authorization.
+  See [Default omission](#jsoninclude-and-default-omission).
 
 ```java
 public final class Response {
@@ -234,13 +235,21 @@ with `ALWAYS`. `defaultPropertyInclusion(NON_DEFAULT)` immediately throws a conf
 global authorization would silently apply this contract to unrelated models. `NON_EMPTY` does not
 grant default-evaluation authorization.
 
-| Model                                                                                                                     | Supported default source                                                                   |
-| ------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
-| Scala constructor property                                                                                                | Declared compiler default method, evaluated on each write with actual preceding parameters |
-| Java ordinary no-argument model                                                                                           | One reference object constructed using the reader's no-argument constructor                |
-| Kotlin selected constructor with defaults for every parameter                                                             | One reference object constructed using those language defaults                             |
-| Ordinary Kotlin no-argument model                                                                                         | One reference object using that constructor                                                |
-| Java required constructor parameters, Kotlin constructor with any required parameter, or Scala missing default/dependency | Rejected for an authorized property                                                        |
+Both field-level and class-level `NON_DEFAULT` retain properties without defaults, including required
+constructor parameters and Kotlin `lateinit` properties. Their null, zero, false, and empty values are written;
+reader fallbacks do not make them defaulted properties. Java models without a no-argument
+construction path selected by the reader retain their authorized properties.
+Use `ALWAYS` to retain a property with an unstable or unsupported default. Mixins follow the same rules.
+
+| Model                                                                                | Supported default source                                                                   |
+| ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------ |
+| Scala constructor property                                                           | Declared compiler default method, evaluated on each write with actual preceding parameters |
+| Java ordinary no-argument model                                                      | One reference object constructed using the reader's no-argument constructor                |
+| Kotlin selected constructor with defaults for every parameter                        | One reference object constructed using those language defaults                             |
+| Ordinary Kotlin no-argument model                                                    | One reference object using that constructor                                                |
+| Required properties without defaults                                                 | Retained under both field-level and class-level authorization                              |
+| Kotlin defaulted property when the selected constructor also has required parameters | Rejected: no reference object can be constructed without inventing required arguments      |
+| Scala default method with unavailable dependencies                                   | Rejected                                                                                   |
 
 Reference construction happens once per initialized model metadata, not once per serialization.
 Distinct declared and dynamic model occurrences can initialize separately. Authorization permits
diff --git a/docs/json/kotlin.md b/docs/json/kotlin.md
index 54a4b1c0a..8dedacdfa 100644
--- a/docs/json/kotlin.md
+++ b/docs/json/kotlin.md
@@ -202,15 +202,21 @@ Explicit null remains distinct from a missing field and is rejected for a non-nu
 Choose an inclusion rule that retains values when exact round trips are required.
 
 `NON_DEFAULT` requires explicit field authorization with `@JsonProperty`, or class authorization
-with `@JsonInclude`. It is supported when **every parameter of the selected constructor has a
-language default**, or the model uses an ordinary no-argument constructor. Fory constructs one
+with `@JsonInclude`. Comparing against a reference object is supported when **every parameter of the
+selected constructor has a language default**, or the model uses an ordinary no-argument constructor. Fory constructs one
 reference object per initialized model metadata and captures authorized property values. This
 executes the complete constructor, all initializers, and `init` blocks. It is not repeated on each
 write, and models without authorization do not construct a reference. Dynamic and declared model
 occurrences can initialize separately.
 
-Models with required parameters are rejected, even if another constructor happens to have no
-arguments. Fory does not fabricate parameters, borrow them from the first object, change creator
+Both field-level and class-level authorization retain constructor properties without defaults and
+`lateinit` properties, including null, zero, false, and empty values. A defaulted property that needs
+a reference object is rejected when the selected constructor also has required parameters, even if
+another constructor has no arguments.
+For example, class-level `NON_DEFAULT` on `data class Key(val id: Int)` writes `{"id":0}` for `Key(0)`.
+For `data class Entry(val id: Int, val count: Int = 1)`, the same class policy cannot compare `count`
+without inventing `id`; give `count` an `ALWAYS` override to retain it.
+Fory does not fabricate parameters, borrow them from the first object, change creator
 selection, bypass a constructor, or analyze bytecode. Failed construction reports the model and
 property. Global `defaultPropertyInclusion(NON_DEFAULT)` is rejected.
 
diff --git a/docs/json/object-mapping.md b/docs/json/object-mapping.md
index b7d713f12..0410451ef 100644
--- a/docs/json/object-mapping.md
+++ b/docs/json/object-mapping.md
@@ -225,7 +225,8 @@ ForyJson json = ForyJson.builder().defaultPropertyInclusion(Include.NON_EMPTY).b
 The builder accepts `ALWAYS`, `NON_NULL`, and `NON_EMPTY`. It rejects `DEFAULT` and `NON_DEFAULT`.
 Default omission requires explicit `@JsonProperty(include = NON_DEFAULT)` or class-level
 `@JsonInclude(NON_DEFAULT)` authorization; see [Default omission](annotations.md#jsoninclude-and-default-omission)
-for supported sources, construction/evaluation effects, and errors. An explicit `JsonProperty.include`
+for supported sources, construction/evaluation effects, and errors. Both forms retain properties
+without defaults, including null and zero values. An explicit `JsonProperty.include`
 overrides `JsonInclude` on the class, which overrides the builder default. See
 [Property inclusion](annotations.md#jsonproperty) for the empty-value definitions and boundaries.
 
diff --git a/docs/json/scala.md b/docs/json/scala.md
index 789d58f14..38288eb89 100644
--- a/docs/json/scala.md
+++ b/docs/json/scala.md
@@ -147,7 +147,7 @@ import org.apache.fory.json.scala.ForyJsonScala
 
 @JsonInclude(Include.NON_DEFAULT)
 case class Request(
-  @JsonProperty(include = Include.ALWAYS) id: Int,
+  id: Int,
   retries: Int = 3,
   tags: List[String] = Nil
 )
@@ -158,22 +158,24 @@ json.fromJson("""{"id":0}""", classOf[Request]) // Request(0, 3, Nil)
 json.toJson(Request(0, retries = 0)) // {"id":0,"retries":0}
 ```
 
-`id` is explicitly excluded because it has no declared default; otherwise class-level authorization
-would fail. A reader's implicit zero, empty collection, or `None` fallback is not a declared default.
+`id` has no declared default, so class-level authorization keeps it in the output, including when
+its value is zero. A reader's implicit zero, empty collection, or `None` fallback is not a declared
+default.
 Alternatively, omit the class annotation and place `@JsonProperty(include = Include.NON_DEFAULT)`
 only on selected defaulted properties. Mixins support both forms. Global `NON_DEFAULT` is rejected.
 
 Default expressions run during writing and must be deterministic and free of externally visible
 side effects. For `case class Limits(low: Int)(val high: Int = low + 1)`, the comparison for `high`
 uses the object's actual `low`. For `low=5, high=2`, high is retained because its default is 6.
-Missing default methods or unavailable write-schema dependencies cause a model-initialization error.
-`Unit` defaults whose JVM methods return `void` are not supported comparison sources; retain those
-properties with `ALWAYS` when authorizing the class.
+Unavailable dependencies of a selected default method cause a model-initialization error.
+Properties without a supported compiler default method are retained under both class-level and
+field-level `NON_DEFAULT`. This also applies to `Unit` defaults whose JVM methods return `void`,
+which are not supported comparison sources.
 Authorization confirms that missing input restores the same context; Fory does not prove this or
 expression purity. Use `ALWAYS` for time-, random-, or state-dependent defaults. Arrays compare by contents, and
 floating-point comparisons distinguish positive and negative zero. Class-body initializers are
-not inferred as Scala constructor defaults. Values differing from a default, including null and
-empty collections, remain written. Reading stays independent and creates fresh mutable defaults.
+not inferred as Scala constructor defaults and remain written. Values differing from a default,
+including null and empty collections, remain written. Reading stays independent and creates fresh mutable defaults.
 Class authorization covers future added fields too; see [Default omission](annotations.md#jsoninclude-and-default-omission).
 
 ## Supported Scala types
diff --git a/java/fory-annotation-processor/src/test/java/org/apache/fory/annotation/processing/JsonTypeProcessorTest.java b/java/fory-annotation-processor/src/test/java/org/apache/fory/annotation/processing/JsonTypeProcessorTest.java
index 49ec85cc2..808b17549 100644
--- a/java/fory-annotation-processor/src/test/java/org/apache/fory/annotation/processing/JsonTypeProcessorTest.java
+++ b/java/fory-annotation-processor/src/test/java/org/apache/fory/annotation/processing/JsonTypeProcessorTest.java
@@ -606,6 +606,69 @@ public class JsonTypeProcessorTest {
     }
   }
 
+  @Test
+  public void requiredInclusion() throws Exception {
+    for (boolean mixin : new boolean[] {false, true}) {
+      String inclusion = "@JsonInclude(JsonProperty.Include.NON_DEFAULT) ";
+      CompilationResult result =
+          compile(
+              "test.RequiredModel",
+              "package test;\nimport org.apache.fory.json.annotation.*;\n"
+                  + (mixin ? "" : "@JsonType " + inclusion)
+                  + "public class RequiredModel {\n"
+                  + "  public static int calls;\n"
+                  + "  public final int count;\n"
+                  + "  public final String label;\n"
+                  + "  public RequiredModel() { this(3, \"default\"); }\n"
+                  + "  @JsonCreator public RequiredModel(@JsonProperty(\"count\") int count, "
+                  + "@JsonProperty(\"label\") String label) {\n"
+                  + "    calls++; this.count = count; this.label = label;\n  }\n}\n"
+                  + (mixin
+                      ? "@JsonMixin(target=RequiredModel.class) "
+                          + inclusion
+                          + "abstract class RequiredMixin {}\n"
+                      : ""));
+      assertTrue(result.success, result.diagnostics());
+      ClassLoader loader = result.classLoader();
+      Class<?> type = loader.loadClass("test.RequiredModel");
+      GeneratedJsonCodec<?> codec =
+          generatedCodec(
+              loader,
+              mixin
+                  ? "test.RequiredMixin_ForyJsonMixin_test_x2e_RequiredModel_ForyJsonCodec"
+                  : "test.RequiredModel_ForyJsonCodec");
+      Object value = type.getConstructor(int.class, String.class).newInstance(0, null);
+      assertEquals(fieldAccessor(codec.fieldAccessors(), "count").getInt(value), 0);
+      for (boolean codegen : new boolean[] {false, true}) {
+        org.apache.fory.json.ForyJsonBuilder builder =
+            ForyJson.builder()
+                .withClassLoader(loader)
+                .withCodegen(codegen)
+                .withAsyncCompilation(false);
+        if (mixin) {
+          builder.registerMixin(loader.loadClass("test.RequiredMixin"));
+        }
+        ForyJson json = builder.build();
+        int calls = type.getField("calls").getInt(null);
+        String compact = "{\"count\":0,\"label\":null}";
+        String pretty = "{\n  \"count\" : 0,\n  \"label\" : null\n}";
+        assertEquals(json.toJson(value), compact);
+        assertEquals(new String(json.toJsonBytes(value), StandardCharsets.UTF_8), compact);
+        assertEquals(json.toPrettyJson(value), pretty);
+        assertEquals(new String(json.toPrettyJsonBytes(value), StandardCharsets.UTF_8), pretty);
+        assertEquals(type.getField("calls").getInt(null), calls);
+        for (Object decoded :
+            new Object[] {
+              json.fromJson(compact, type),
+              json.fromJson(pretty.getBytes(StandardCharsets.UTF_8), type)
+            }) {
+          assertEquals(type.getField("count").getInt(decoded), 0);
+          assertEquals(type.getField("label").get(decoded), null);
+        }
+      }
+    }
+  }
+
   @Test
   public void generatedAccessors() throws Exception {
     CompilationResult result =
diff --git a/java/fory-json/src/main/java/org/apache/fory/json/annotation/JsonInclude.java b/java/fory-json/src/main/java/org/apache/fory/json/annotation/JsonInclude.java
index 3a79369ec..971873aa8 100644
--- a/java/fory-json/src/main/java/org/apache/fory/json/annotation/JsonInclude.java
+++ b/java/fory-json/src/main/java/org/apache/fory/json/annotation/JsonInclude.java
@@ -37,8 +37,14 @@ import java.lang.annotation.Target;
  * every participating property, including fields added later. The caller accepts the evaluation and
  * construction requirements documented on that policy and guarantees stable defaults with
  * equivalent missing-field recovery. Use {@code ALWAYS} on individual properties to exclude them.
- * Every effectively authorized property must have a supported default source; otherwise model
- * initialization fails. {@code NON_EMPTY} does not authorize default evaluation.
+ * Properties without a default remain included, even when null, zero, false, or empty. This
+ * includes required constructor properties and Kotlin lateinit properties. Java models without a
+ * reader no-argument construction path retain their properties. A declared default that cannot be
+ * evaluated through a supported source still causes an error; for example, a Kotlin defaulted
+ * property cannot use a reference object if the selected constructor also requires arguments.
+ * Property-level {@code NON_DEFAULT} follows the same rules. Scala properties without a supported
+ * compiler default method also remain included. {@code NON_EMPTY} does not authorize default
+ * evaluation.
  */
 @Documented
 @Retention(RetentionPolicy.RUNTIME)
diff --git a/java/fory-json/src/main/java/org/apache/fory/json/annotation/JsonProperty.java b/java/fory-json/src/main/java/org/apache/fory/json/annotation/JsonProperty.java
index 1b7e5d430..8b5c856ab 100644
--- a/java/fory-json/src/main/java/org/apache/fory/json/annotation/JsonProperty.java
+++ b/java/fory-json/src/main/java/org/apache/fory/json/annotation/JsonProperty.java
@@ -122,8 +122,12 @@ public @interface JsonProperty {
      * <p>Kotlin metadata does not expose default expressions or dependencies. A reference object
      * cannot represent defaults that vary with the current object's parameters, time, or external
      * state. Authorize only properties for which its baseline matches missing-field recovery.
-     * Unsupported sources and failed construction cause a model-initialization error, rather than
-     * silently retaining the field or bypassing the constructor.
+     * Properties without defaults remain included under both property-level and class-level
+     * authorization, including required constructor parameters and Kotlin lateinit properties. Java
+     * models without a reader no-argument construction path retain their authorized properties.
+     * Scala properties without a supported compiler default method also remain included. Declared
+     * defaults requiring an unavailable reference object, or failed construction/evaluation, still
+     * cause errors.
      *
      * <p>Primitive values compare directly; floating-point values preserve signed zero and never
      * omit non-finite values. References use {@code equals}, and arrays compare contents with exact
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 dd010892a..251d11255 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
@@ -531,7 +531,28 @@ final class ObjectCodecBuilder {
       // Authorization never turns reader type fallbacks into declared defaults. Only the
       // selected no-arg/all-default construction path can supply a reference value.
       if (model != null && !model.referenceDefaults()) {
-        throw unsupportedDefault(type, field, "the property has no declared constructor default");
+        return;
+      }
+      // Both field and class omission retain properties without defaults, including required
+      // constructor arguments and Kotlin lateinit properties. Keep null/zero/empty values too.
+      if (builder.requiredDeferred
+          || (creator != null && index >= 0 && creator.defaultMaskBit(index) < 0)) {
+        return;
+      }
+      if (model == null) {
+        if (creator != null) {
+          // A Java parameterized creator supplies no default baseline, even if the class also
+          // declares a no-argument constructor. Do not change the reader's creator selection.
+          if (!(creator.executable() instanceof Constructor) || creator.argumentCount() != 0) {
+            return;
+          }
+        } else {
+          try {
+            type.getDeclaredConstructor();
+          } catch (NoSuchMethodException e) {
+            return;
+          }
+        }
       }
       referenceDefaults.add(field);
     }
diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonInclusionTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonInclusionTest.java
index 178023f13..763b8c941 100644
--- a/java/fory-json/src/test/java/org/apache/fory/json/JsonInclusionTest.java
+++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonInclusionTest.java
@@ -134,8 +134,20 @@ public class JsonInclusionTest extends ForyJsonTestModels {
     assertNotSame(first.values, second.values);
     first.values.add(2);
     assertEquals(second.values, singletonList(1));
-    assertThrows(ForyJsonException.class, () -> json.toJson(new ExplicitDefaults(0, null)));
-    assertThrows(ForyJsonException.class, () -> json.toJson(new RequiredConstructor(1)));
+    ExplicitDefaults required = new ExplicitDefaults(0, null);
+    assertJson(json, required, "{\"count\":0,\"label\":null}");
+    String requiredPretty = "{\n  \"count\" : 0,\n  \"label\" : null\n}";
+    assertEquals(json.toPrettyJson(required), requiredPretty);
+    assertEquals(
+        new String(json.toPrettyJsonBytes(required), StandardCharsets.UTF_8), requiredPretty);
+    assertEquals(json.fromJson(requiredPretty, ExplicitDefaults.class).count, 0);
+    assertEquals(
+        json.fromJson(requiredPretty.getBytes(StandardCharsets.UTF_8), ExplicitDefaults.class)
+            .label,
+        null);
+    assertGeneratedWhenSupported(json, ExplicitDefaults.class, codegenEnabled());
+    assertJson(json, new RequiredConstructor(0), "{\"count\":0}");
+    assertGeneratedWhenSupported(json, RequiredConstructor.class, codegenEnabled());
     assertGeneratedWhenSupported(json, Initialized.class, codegenEnabled());
   }
 
@@ -216,7 +228,12 @@ public class JsonInclusionTest extends ForyJsonTestModels {
     value.value = new EmptyObject();
     assertJson(json, value, "{\"value\":\"\"}");
     assertEquals(lookups.calls, 2);
-    assertThrows(ForyJsonException.class, () -> resolver.getObjectCodec(RequiredConstructor.class));
+    FailingDefault.fail = true;
+    try {
+      assertThrows(ForyJsonException.class, () -> resolver.getObjectCodec(FailingDefault.class));
+    } finally {
+      FailingDefault.fail = false;
+    }
     assertJson(json, value, "{\"value\":\"\"}");
     assertEquals(lookups.calls, 3);
     assertGeneratedWhenSupported(json, Dynamic.class, codegenEnabled());
@@ -299,6 +316,34 @@ public class JsonInclusionTest extends ForyJsonTestModels {
     }
   }
 
+  @Test
+  public void requiredMixin() {
+    ForyJson json = newJsonBuilder().registerMixin(RequiredMixin.class).build();
+    ConstructorOnly value = new ConstructorOnly(0);
+    int calls = ConstructorOnly.calls;
+    assertJson(json, value, "{\"count\":0}");
+    assertEquals(json.toPrettyJson(value), "{\n  \"count\" : 0\n}");
+    assertEquals(
+        new String(json.toPrettyJsonBytes(value), StandardCharsets.UTF_8),
+        json.toPrettyJson(value));
+    assertEquals(ConstructorOnly.calls, calls);
+    assertGeneratedWhenSupported(json, ConstructorOnly.class, codegenEnabled());
+  }
+
+  public static class ConstructorOnly {
+    public static int calls;
+    public int count;
+
+    public ConstructorOnly(int count) {
+      calls++;
+      this.count = count;
+    }
+  }
+
+  @JsonMixin(target = ConstructorOnly.class)
+  @JsonInclude(Include.NON_DEFAULT)
+  public abstract static class RequiredMixin {}
+
   public static class FailingDefault {
     public static boolean fail;
 
diff --git a/kotlin/fory-json-kotlin/src/test/kotlin/org/apache/fory/json/kotlin/KotlinDefaultArgumentsTest.kt b/kotlin/fory-json-kotlin/src/test/kotlin/org/apache/fory/json/kotlin/KotlinDefaultArgumentsTest.kt
index efe164e0a..cf66928f6 100644
--- a/kotlin/fory-json-kotlin/src/test/kotlin/org/apache/fory/json/kotlin/KotlinDefaultArgumentsTest.kt
+++ b/kotlin/fory-json-kotlin/src/test/kotlin/org/apache/fory/json/kotlin/KotlinDefaultArgumentsTest.kt
@@ -77,9 +77,25 @@ class KotlinDefaultArgumentsTest {
 
   @JsonInclude(Include.NON_DEFAULT)
   class DeferredDefault(val count: Int = 3) {
-    @JsonProperty(include = Include.ALWAYS) lateinit var required: String
+    lateinit var required: String
   }
 
+  @JsonInclude(Include.NON_DEFAULT)
+  data class RequiredValues(
+    val count: Int,
+    @JsonProperty(include = Include.NON_DEFAULT) val flag: Boolean,
+    val label: String?,
+    val items: List<Int>
+  )
+
+  @JsonMixin(target = RequiredDefault::class)
+  @JsonInclude(Include.NON_DEFAULT)
+  abstract class RequiredDefaultMixin(
+    val required: Int,
+    // Match the target's default use sites: Kotlin 2.4 also annotates the backing field.
+    @JsonProperty(include = Include.ALWAYS) val value: Int
+  )
+
   data class PlainDefault(val value: Int = 1) {
     init {
       constructions++
@@ -145,7 +161,8 @@ class KotlinDefaultArgumentsTest {
         json.fromJson("{\"values\":null}".encodeToByteArray(), ReferenceDefault::class.java)
       }
       assertFailsWith<ForyJsonException> { json.toJson(RequiredDefault(1)) }
-      assertFailsWith<ForyJsonException> { json.toJson(SelectedCreator()) }
+      assertEquals("{\"value\":1}", json.toJson(SelectedCreator()))
+      assertEquals("{\"value\":0}", json.toJsonBytes(SelectedCreator(0)).decodeToString())
       val failing = FailingDefault()
       FailingDefault.fail = true
       try {
@@ -170,6 +187,37 @@ class KotlinDefaultArgumentsTest {
     }
   }
 
+  @Test
+  fun requiredInclusion() {
+    for (codegen in listOf(false, true)) {
+      val json = ForyJsonKotlin.builder().withCodegen(codegen).withAsyncCompilation(false).build()
+      val value = RequiredValues(0, false, null, emptyList())
+      val expected = "{\"count\":0,\"flag\":false,\"label\":null,\"items\":[]}"
+      val pretty =
+        "{\n  \"count\" : 0,\n  \"flag\" : false,\n  \"label\" : null,\n  \"items\" : [ ]\n}"
+      assertEquals(expected, json.toJson(value))
+      assertEquals(expected, json.toJsonBytes(value).decodeToString())
+      assertEquals(pretty, json.toPrettyJson(value))
+      assertEquals(pretty, json.toPrettyJsonBytes(value).decodeToString())
+      assertEquals(value, json.fromJson(expected, RequiredValues::class.java))
+      assertEquals(value, json.fromJson(pretty.encodeToByteArray(), RequiredValues::class.java))
+      assertWriterGeneration(json, RequiredValues::class.java, codegen)
+
+      val mixed =
+        ForyJsonKotlin.builder()
+          .withCodegen(codegen)
+          .withAsyncCompilation(false)
+          .registerMixin(RequiredDefaultMixin::class.java)
+          .build()
+      assertEquals("{\"required\":0,\"value\":1}", mixed.toJson(RequiredDefault(0)))
+      assertEquals(
+        "{\"required\":0,\"value\":1}",
+        mixed.toJsonBytes(RequiredDefault(0)).decodeToString()
+      )
+      assertWriterGeneration(mixed, RequiredDefault::class.java, codegen)
+    }
+  }
+
   @Test
   fun deferredDefaultInclusion() {
     for (codegen in listOf(false, true)) {
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 8722da15b..c8bc44d63 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
@@ -76,6 +76,7 @@ case class BodyState(id: Int) {
   var count: Int = 7
 }
 
+@JsonInclude(JsonProperty.Include.NON_DEFAULT)
 case class CurriedDefault(a: Int)(
     @JsonProperty(include = JsonProperty.Include.NON_DEFAULT) val b: Int = a + 1
 )
@@ -86,9 +87,9 @@ case class KeywordDefault(switch: Int)(
 
 @JsonInclude(JsonProperty.Include.NON_DEFAULT)
 case class InclusionDefaults(
-    @JsonProperty(include = JsonProperty.Include.ALWAYS) required: Int,
-    @JsonProperty(include = JsonProperty.Include.ALWAYS) flag: Boolean,
-    @JsonProperty(include = JsonProperty.Include.ALWAYS) optional: Option[Int],
+    required: Int,
+    flag: Boolean,
+    optional: Option[Int],
     number: Int = 2,
     currency: String = "USD",
     selected: Option[Int] = Some(7),
@@ -178,6 +179,16 @@ case class HiddenDependency(@JsonIgnore a: Int = 5)(
 @JsonInclude(JsonProperty.Include.NON_DEFAULT)
 case class MissingDeclaredDefault(value: Option[Int])
 
+case class RequiredMixinValue(id: String, count: Int = 3)
+
+@JsonMixin(target = classOf[RequiredMixinValue])
+@JsonInclude(JsonProperty.Include.NON_DEFAULT)
+abstract class RequiredDefaultMixin
+
+case class ExplicitMissingDefault(
+    @JsonProperty(include = JsonProperty.Include.NON_DEFAULT) value: Option[Int]
+)
+
 @JsonInclude(JsonProperty.Include.NON_DEFAULT)
 case class BodyDefault() {
   var count: Int = 3
@@ -875,6 +886,9 @@ class ScalaJsonSuite extends AnyFunSuite {
       val expected = "{\"required\":0,\"flag\":false,\"optional\":null}"
       assert(json.toJson(value) == expected)
       assert(new String(json.toJsonBytes(value), UTF_8) == expected)
+      val pretty = "{\n  \"required\" : 0,\n  \"flag\" : false,\n  \"optional\" : null\n}"
+      assert(json.toPrettyJson(value) == pretty)
+      assert(new String(json.toPrettyJsonBytes(value), UTF_8) == pretty)
       assert(reader.fromJson(expected, classOf[InclusionDefaults]) == value)
       assert(reader.fromJson(json.toPrettyJson(value), classOf[InclusionDefaults]) == value)
       assert(reader.fromJson(json.toPrettyJsonBytes(value), classOf[InclusionDefaults]) == value)
@@ -885,6 +899,7 @@ class ScalaJsonSuite extends AnyFunSuite {
       assert(reader.fromJson(text, classOf[InclusionDefaults]) == changed)
       assert(json.toJson(RetainedDefault()) == "{\"value\":2}")
       assert(json.toJson(NestedModels.OptionalOnly(None)) == "{\"value\":null}")
+      if (!async) assertWriterGeneration(json, classOf[InclusionDefaults], codegen)
     }
   }
 
@@ -954,12 +969,24 @@ class ScalaJsonSuite extends AnyFunSuite {
       assert(text.contains("\"currency\":\"USD\""))
       assert(new String(mixin.toJsonBytes(MixinDefaults()), UTF_8) == text)
       intercept[ForyJsonException](json.toJson(HiddenDependency()()))
-      intercept[ForyJsonException](json.toJson(MissingDeclaredDefault(None)))
-      intercept[ForyJsonException](json.toJson(BodyDefault()))
-      intercept[ForyJsonException](json.toJsonBytes(BodyDefault()))
-      // A JVM void default is not a callable value source in the current constructor model.
-      intercept[ForyJsonException](json.toJson(UnitDefault()))
-      intercept[ForyJsonException](json.toJsonBytes(UnitDefault()))
+      assert(json.toJson(MissingDeclaredDefault(None)) == "{\"value\":null}")
+      assert(new String(json.toJsonBytes(MissingDeclaredDefault(None)), UTF_8) == "{\"value\":null}")
+      assert(json.toJson(BodyDefault()) == "{\"count\":3}")
+      assert(new String(json.toJsonBytes(BodyDefault()), UTF_8) == "{\"count\":3}")
+      assert(json.toJson(ExplicitMissingDefault(None)) == "{\"value\":null}")
+      assert(new String(json.toJsonBytes(ExplicitMissingDefault(None)), UTF_8) == "{\"value\":null}")
+      val requiredMixin = ForyJsonScala.builder().withCodegen(codegen)
+        .withAsyncCompilation(false).registerMixin(classOf[RequiredDefaultMixin]).build()
+      val required = RequiredMixinValue("漢")
+      val requiredText = "{\"id\":\"漢\"}"
+      assert(requiredMixin.toJson(required) == requiredText)
+      assert(new String(requiredMixin.toJsonBytes(required), UTF_8) == requiredText)
+      assert(requiredMixin.fromJson(requiredText, classOf[RequiredMixinValue]) == required)
+      assert(requiredMixin.fromJson(requiredMixin.toPrettyJsonBytes(required), classOf[RequiredMixinValue]) == required)
+      assertWriterGeneration(requiredMixin, classOf[RequiredMixinValue], codegen)
+      // A JVM void method supplies no comparable default value; retain the Unit property.
+      assert(json.toJson(UnitDefault()) == "{\"value\":null}")
+      assert(new String(json.toJsonBytes(UnitDefault()), UTF_8) == "{\"value\":null}")
       val fresh1 = json.fromJson("{}", classOf[FreshDefaults])
       val fresh2 = json.fromJson("{}".getBytes(UTF_8), classOf[FreshDefaults])
       fresh1.values += 2


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