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