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 03f3b4aff feat(java): add json property order annotation (#3840)
03f3b4aff is described below
commit 03f3b4aff108c161fa7c79690fc5b25e7263fc0b
Author: Shawn Yang <[email protected]>
AuthorDate: Tue Jul 14 11:09:35 2026 +0530
feat(java): add json property order annotation (#3840)
## Why?
## What does this PR do?
## Related issues
## AI Contribution Checklist
- [ ] Substantial AI assistance was used in this PR: `yes` / `no`
- [ ] If `yes`, I included a completed [AI Contribution
Checklist](https://github.com/apache/fory/blob/main/AI_POLICY.md#9-contributor-checklist-for-ai-assisted-prs)
in this PR description and the required `AI Usage Disclosure`.
- [ ] If `yes`, my PR description includes the required `ai_review`
summary and screenshot evidence or equivalent persisted links of the
final clean AI review results from both fresh reviewers described in
`AI_POLICY.md`, the Fory-guided reviewer and the independent general
reviewer, on the current PR diff or current HEAD after the latest code
changes.
## Does this PR introduce any user-facing change?
- [ ] Does this PR introduce any public API change?
- [ ] Does this PR introduce any binary protocol compatibility change?
## Benchmark
---
docs/guide/java/json-support.md | 68 ++-
java/fory-json/README.md | 63 ++-
.../apache/fory/json/annotation/JsonProperty.java | 26 +-
.../fory/json/annotation/JsonPropertyOrder.java | 56 +++
.../org/apache/fory/json/codec/ObjectCodec.java | 255 ++++++++++-
.../apache/fory/json/JsonPropertyOrderTest.java | 486 +++++++++++++++++++++
.../org/apache/fory/json/JsonSubTypesTest.java | 14 +-
7 files changed, 925 insertions(+), 43 deletions(-)
diff --git a/docs/guide/java/json-support.md b/docs/guide/java/json-support.md
index fe1da7a90..6bb3038f7 100644
--- a/docs/guide/java/json-support.md
+++ b/docs/guide/java/json-support.md
@@ -113,8 +113,9 @@ public final class JsonExample {
}
```
-Unknown input properties are skipped. Null object properties are omitted by
default. JSON member
-order is not a compatibility contract.
+Unknown input properties are skipped. Null object properties are omitted by
default. Default JSON
+member discovery order is not a compatibility contract; use
`JsonPropertyOrder` or
+`JsonProperty.index` when emitted member order must be explicit.
## Reading and writing
@@ -298,14 +299,16 @@ output size. Builder changes after `build()` do not
mutate an existing runtime.
## Annotations
-Fory JSON provides only `JsonProperty`, `JsonIgnore`, `JsonCreator`, and
`JsonSubTypes` under
-`org.apache.fory.json.annotation`. They are not Jackson, Gson, or Fory
binary-protocol annotations.
+Fory JSON provides `JsonProperty`, `JsonPropertyOrder`, `JsonIgnore`,
`JsonCreator`, and
+`JsonSubTypes` under `org.apache.fory.json.annotation`. They are not Jackson,
Gson, or Fory
+binary-protocol annotations.
```java
import org.apache.fory.json.PropertyNamingStrategy;
import org.apache.fory.json.annotation.JsonCreator;
import org.apache.fory.json.annotation.JsonIgnore;
import org.apache.fory.json.annotation.JsonProperty;
+import org.apache.fory.json.annotation.JsonPropertyOrder;
import org.apache.fory.json.annotation.JsonSubTypes;
```
@@ -319,6 +322,9 @@ private long id;
@JsonProperty(include = JsonProperty.Include.ALWAYS)
private String displayName;
+
+@JsonProperty(index = 10)
+private String email;
```
Supported inclusion values are:
@@ -327,9 +333,59 @@ Supported inclusion values are:
- `ALWAYS`: include null;
- `NON_NULL`: omit null.
+`index` controls relative serialization order. Indexed properties are written
in ascending index
+order before unindexed properties. Indexes must be non-negative, may contain
gaps, and must be
+unique among writable properties. `-1` means unspecified; lower values are
invalid. An index on a
+setter-only, creator-only, or write-ignored property is invalid.
+
Inclusion affects writing only. Identical repeated declarations are allowed;
conflicting explicit
-names or non-default policies fail. Two properties cannot normalize to the
same JSON name.
-`NON_EMPTY`, aliases, formatting, and annotation-selected codecs are
unsupported.
+names, indexes, or non-default policies fail. Two properties cannot normalize
to the same JSON
+name. `NON_EMPTY`, aliases, formatting, and annotation-selected codecs are
unsupported.
+
+### `JsonPropertyOrder`
+
+Use `JsonPropertyOrder` to combine a named prefix, property indexes, and
final-name alphabetic
+ordering:
+
+```java
+@JsonPropertyOrder(value = {"id", "display_name"}, alphabetic = true)
+public final class User {
+ @JsonProperty(index = 20)
+ public String name;
+
+ @JsonProperty(value = "display_name", index = 10)
+ public String displayName;
+
+ public long id;
+ public int age;
+ public String address;
+}
+```
+
+The output order is `id`, `display_name`, `name`, `address`, then `age`:
+
+```json
+{ "id": 1, "display_name": "Alice", "name": "alice", "address": "x", "age": 30
}
+```
+
+The named prefix is written first. Remaining indexed properties follow in
ascending index order.
+When `alphabetic = true`, remaining unindexed properties are sorted by final
JSON name; otherwise
+they keep their existing relative order. Use `@JsonPropertyOrder(alphabetic =
true)` when no named
+prefix is needed. Alphabetic comparison uses Java's natural, case-sensitive
String order and does
+not depend on the locale.
+
+Order entries match the final JSON name first and the Java logical property
name second. This lets
+`display_name` match an explicit `JsonProperty` name while an unannotated
`displayName` can still be
+addressed by either `display_name` under `SNAKE_CASE` or its Java name
`displayName`.
+
+The list may be empty only when `alphabetic` is true. Its entries must be
non-empty, unique writable
+properties; unknown and duplicate entries fail when the object metadata is
built. A subclass
+declaration replaces both settings from its superclass as a whole;
declarations are not merged. If
+the subclass has no declaration, the nearest superclass declaration is used
and resolved against the
+subclass properties. Interface declarations are not considered.
+
+Property order affects serialization only. Deserialization remains name-based
and accepts members
+in any order. Subtype discriminators remain before user properties.
### Naming strategy
diff --git a/java/fory-json/README.md b/java/fory-json/README.md
index 126d11e48..6b0770d48 100644
--- a/java/fory-json/README.md
+++ b/java/fory-json/README.md
@@ -95,8 +95,9 @@ public final class JsonExample {
}
```
-Unknown input properties are skipped. Null object properties are omitted by
default. JSON property
-order is not a compatibility contract; consumers should match names, not
textual member order.
+Unknown input properties are skipped. Null object properties are omitted by
default. Default JSON
+property discovery order is not a compatibility contract; use
`JsonPropertyOrder` or
+`JsonProperty.index` when emitted property order must be explicit.
## Reading and writing APIs
@@ -355,13 +356,14 @@ Builder mutation after `build()` does not modify an
existing `ForyJson` runtime.
## JSON annotations
-Fory JSON defines four annotations in `org.apache.fory.json.annotation`. They
are Fory JSON APIs,
+Fory JSON defines five annotations in `org.apache.fory.json.annotation`. They
are Fory JSON APIs,
not Jackson, Gson, or Fory binary-protocol compatibility annotations.
### `JsonProperty`
-`JsonProperty` configures the canonical name and null inclusion of one
complete logical property.
-An annotation on a field, getter, or setter applies to the merged
field/getter/setter group.
+`JsonProperty` configures the canonical name, serialization index, and null
inclusion of one
+complete logical property. An annotation on a field, getter, or setter applies
to the merged
+field/getter/setter group.
```java
import org.apache.fory.json.annotation.JsonProperty;
@@ -373,6 +375,9 @@ public final class User {
@JsonProperty(include = JsonProperty.Include.ALWAYS)
private String displayName;
+ @JsonProperty(index = 10)
+ private String email;
+
public long getId() {
return id;
}
@@ -383,20 +388,64 @@ public final class User {
}
```
-Version 1 supports three inclusion values:
+The supported inclusion values are:
- `DEFAULT`: use `ForyJsonBuilder.writeNullFields`.
- `ALWAYS`: write the property even when its selected value is null.
- `NON_NULL`: omit a null value.
Inclusion affects writing only. A non-default inclusion is invalid for a
creator-only property with
-no write source. Repeating the same declaration is allowed; conflicting
explicit names or
+no write source. Repeating the same declaration is allowed; conflicting
explicit names, indexes, or
non-default inclusion policies within one logical property are rejected. Two
properties that
normalize to the same final JSON name are also rejected.
+`index` controls relative serialization order. Indexed properties are written
in ascending index
+order before unindexed properties. Indexes must be non-negative, may contain
gaps, and must be
+unique among writable properties. `-1` means unspecified; lower values are
invalid. An index on a
+setter-only, creator-only, or write-ignored property is invalid.
+
`NON_EMPTY`, aliases, formatting, annotation-selected codecs, and independent
read/write names are
not supported.
+### `JsonPropertyOrder`
+
+`JsonPropertyOrder` combines a named serialization prefix, property indexes,
and final-name
+alphabetic ordering:
+
+```java
+import org.apache.fory.json.annotation.JsonProperty;
+import org.apache.fory.json.annotation.JsonPropertyOrder;
+
+@JsonPropertyOrder(value = {"id", "display_name"}, alphabetic = true)
+public final class User {
+ @JsonProperty(index = 20)
+ public String name;
+
+ @JsonProperty(value = "display_name", index = 10)
+ public String displayName;
+
+ public long id;
+ public int age;
+ public String address;
+}
+```
+
+The output order is `id`, `display_name`, `name`, `address`, then `age`. The
named prefix is written
+first, remaining indexed properties follow in ascending index order, and
`alphabetic = true` sorts
+the remaining unindexed properties by final JSON name. Without `alphabetic`,
those properties keep
+their existing relative order. Use `@JsonPropertyOrder(alphabetic = true)`
when no named prefix is
+needed. Alphabetic comparison uses Java's natural, case-sensitive String order
and is
+locale-independent.
+
+Order entries match the final JSON name first and the Java logical property
name second. The list
+may be empty only when `alphabetic` is true. Its entries must be non-empty,
unique writable
+properties; unknown and duplicate entries fail when object metadata is built.
+
+A subclass declaration replaces both settings from its superclass as a whole.
If the subclass has
+no declaration, the nearest superclass declaration is used and resolved
against the subclass
+properties. Interface declarations are not considered. Ordering affects
serialization only;
+deserialization remains name-based, and subtype discriminators remain before
user properties.
+
### Property naming strategy
Configure the naming style for logical properties without an explicit
non-empty `JsonProperty`
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 c74f8b99c..3561e649a 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
@@ -26,12 +26,13 @@ import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
- * Configures the canonical JSON name and null-inclusion policy of one logical
property.
+ * Configures the canonical JSON name, serialization index, and null-inclusion
policy of one logical
+ * property.
*
* <p>Fory JSON first groups an eligible field, getter, and setter by their
Java property name. An
* annotation on any one of those members configures the complete logical
property. Repeating the
- * same explicit name or inclusion policy is allowed; conflicting non-default
declarations are
- * rejected when the object's metadata is built. {@link JsonIgnore} still
removes the configured
+ * same explicit name, index, or inclusion policy is allowed; conflicting
non-default declarations
+ * are rejected when the object's metadata is built. {@link JsonIgnore} still
removes the configured
* read or write direction and cannot be overridden by this annotation.
*
* <p>On a {@link JsonCreator} parameter, this annotation supplies the input
JSON name in
@@ -42,6 +43,9 @@ import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER})
public @interface JsonProperty {
+ /** Value indicating that no serialization index is configured. */
+ int INDEX_UNKNOWN = -1;
+
/**
* Returns the canonical JSON property name.
*
@@ -51,6 +55,20 @@ public @interface JsonProperty {
*/
String value() default "";
+ /**
+ * Returns the relative serialization index of this property.
+ *
+ * <p>Indexed properties not selected by {@link JsonPropertyOrder} are
written in ascending index
+ * order before unindexed properties. Values greater than or equal to zero
are valid and may have
+ * gaps; values below {@link #INDEX_UNKNOWN} are invalid. Different writable
properties must not
+ * use the same index. A non-default index requires a writable field or
getter, including when the
+ * annotation is declared on a setter or matching {@link JsonCreator}
parameter.
+ *
+ * <p>The index affects serialization order only. It is not a field ID, wire
position, array
+ * index, record-component index, or creator-parameter index.
+ */
+ int index() default INDEX_UNKNOWN;
+
/**
* Returns the null-inclusion policy for this property.
*
@@ -61,7 +79,7 @@ public @interface JsonProperty {
*/
Include include() default Include.DEFAULT;
- /** Null-inclusion policies supported by Fory JSON version 1. */
+ /** Null-inclusion policies supported by Fory JSON. */
enum Include {
/** Inherit the runtime's {@code writeNullFields} setting. */
DEFAULT,
diff --git
a/java/fory-json/src/main/java/org/apache/fory/json/annotation/JsonPropertyOrder.java
b/java/fory-json/src/main/java/org/apache/fory/json/annotation/JsonPropertyOrder.java
new file mode 100644
index 000000000..47cea70ad
--- /dev/null
+++
b/java/fory-json/src/main/java/org/apache/fory/json/annotation/JsonPropertyOrder.java
@@ -0,0 +1,56 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.fory.json.annotation;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Defines the serialization order of a class's JSON properties.
+ *
+ * <p>Names are matched first against final JSON property names and then
against Java logical
+ * property names. Unlisted properties with {@link JsonProperty#index()}
follow in ascending index
+ * order. Remaining unindexed properties are sorted by final JSON name when
{@link #alphabetic()} is
+ * true, or retain their existing relative order otherwise.
+ *
+ * <p>The nearest declaration on the concrete class or one of its superclasses
is used. A subclass
+ * declaration replaces its superclass declaration as a whole; arrays are
never merged. Interface
+ * declarations are not considered. The value may be empty only when
alphabetic ordering is enabled.
+ * Unknown, empty-string, and duplicate property entries are rejected when
object metadata is built.
+ *
+ * <p>This annotation affects serialization only. It cannot reorder protocol
metadata such as a
+ * subtype discriminator.
+ */
+@Documented
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
+public @interface JsonPropertyOrder {
+ /** Returns the ordered property prefix, or an empty array for
alphabetic-only ordering. */
+ String[] value() default {};
+
+ /**
+ * Returns whether remaining unindexed properties are sorted by final JSON
property name using
+ * natural, case-sensitive {@link String} order.
+ */
+ boolean alphabetic() default false;
+}
diff --git
a/java/fory-json/src/main/java/org/apache/fory/json/codec/ObjectCodec.java
b/java/fory-json/src/main/java/org/apache/fory/json/codec/ObjectCodec.java
index 65131e0a5..f55d8bdd5 100644
--- a/java/fory-json/src/main/java/org/apache/fory/json/codec/ObjectCodec.java
+++ b/java/fory-json/src/main/java/org/apache/fory/json/codec/ObjectCodec.java
@@ -40,6 +40,7 @@ import org.apache.fory.json.PropertyNamingStrategy;
import org.apache.fory.json.annotation.JsonCreator;
import org.apache.fory.json.annotation.JsonIgnore;
import org.apache.fory.json.annotation.JsonProperty;
+import org.apache.fory.json.annotation.JsonPropertyOrder;
import org.apache.fory.json.meta.JsonCreatorFieldInfo;
import org.apache.fory.json.meta.JsonCreatorInfo;
import org.apache.fory.json.meta.JsonFieldAccessor;
@@ -62,11 +63,11 @@ import org.apache.fory.util.record.RecordUtils;
* Reflection-backed semantic codec and metadata owner for one Java object
type.
*
* <p>Construction discovers eligible fields and JavaBean properties, merges
each
- * field/getter/setter group into one logical property, applies its name,
inclusion, and directional
- * ignore rules, resolves generic member types against the owner {@link
TypeRef}, and builds
- * separate ordered read and write field arrays. Class-valued fields and
properties are never JSON
- * members. Records retain constructor metadata and field defaults; ordinary
objects retain an
- * allocation strategy plus field or accessor sinks.
+ * field/getter/setter group into one logical property, applies its name,
inclusion, serialization
+ * order, and directional ignore rules, resolves generic member types against
the owner {@link
+ * TypeRef}, and builds separate read and write field arrays. Class-valued
fields and properties are
+ * never JSON members. Records retain constructor metadata and field defaults;
ordinary objects
+ * retain an allocation strategy plus field or accessor sinks.
*
* <p>This codec is the interpreted implementation and the semantic fallback.
Only an exact
* raw-class instance of this class is eligible for generated capability
replacement. Parameterized
@@ -138,12 +139,24 @@ public class ObjectCodec<T> implements JsonCodec<T>,
StringObjectWriter<T>, Utf8
record
? rejectRecordCreator(type)
: buildCreatorInfo(type, ownerType, builders,
propertyNamingStrategy);
+ JsonPropertyOrder propertyOrder = findPropertyOrder(type);
+ boolean orderWrites = propertyOrder != null ||
hasIndexedProperty(builders);
List<JsonFieldInfo> writes = new ArrayList<>();
+ List<FieldBuilder> writeBuilders = orderWrites ? new
ArrayList<>(builders.size()) : null;
List<JsonFieldInfo> reads = new ArrayList<>();
List<String> readJavaNames = record ? new ArrayList<>() : null;
Map<String, FieldBuilder> canonicalNames = new LinkedHashMap<>();
Map<Long, String> canonicalHashes = new LinkedHashMap<>();
for (FieldBuilder builder : builders.values()) {
+ if (builder.hasIndex() && !builder.hasWriteSource()) {
+ throw new ForyJsonException(
+ "JSON property index requires a write source for property "
+ + builder.name
+ + " on "
+ + type.getName()
+ + " from "
+ + builder.explicitIndexSource);
+ }
if (!builder.hasWriteSource() && !builder.hasReadSink()) {
if (builder.hasConfiguration()) {
throw new ForyJsonException(
@@ -178,6 +191,9 @@ public class ObjectCodec<T> implements JsonCodec<T>,
StringObjectWriter<T>, Utf8
}
if (builder.hasWriteSource()) {
writes.add(field);
+ if (writeBuilders != null) {
+ writeBuilders.add(builder);
+ }
}
if (creatorInfo == null && builder.hasReadSink()) {
String priorHashName = canonicalHashes.put(field.nameHash(),
field.name());
@@ -194,7 +210,10 @@ public class ObjectCodec<T> implements JsonCodec<T>,
StringObjectWriter<T>, Utf8
}
}
}
- JsonFieldInfo[] writeArray = writes.toArray(new JsonFieldInfo[0]);
+ JsonFieldInfo[] writeArray =
+ writeBuilders == null
+ ? writes.toArray(new JsonFieldInfo[0])
+ : orderWriteFields(type, propertyOrder, writeBuilders, writes);
JsonFieldInfo[] readArray = reads.toArray(new JsonFieldInfo[0]);
for (int i = 0; i < readArray.length; i++) {
readArray[i].setReadIndex(i);
@@ -208,6 +227,139 @@ public class ObjectCodec<T> implements JsonCodec<T>,
StringObjectWriter<T>, Utf8
type, writeArray, readArray, recordNames, creatorInfo, instantiator);
}
+ private static JsonPropertyOrder findPropertyOrder(Class<?> type) {
+ for (Class<?> current = type;
+ current != null && current != Object.class;
+ current = current.getSuperclass()) {
+ JsonPropertyOrder order =
current.getDeclaredAnnotation(JsonPropertyOrder.class);
+ if (order != null) {
+ return order;
+ }
+ }
+ return null;
+ }
+
+ private static boolean hasIndexedProperty(Map<String, FieldBuilder>
builders) {
+ for (FieldBuilder builder : builders.values()) {
+ if (builder.hasIndex()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static JsonFieldInfo[] orderWriteFields(
+ Class<?> type,
+ JsonPropertyOrder propertyOrder,
+ List<FieldBuilder> builders,
+ List<JsonFieldInfo> fields) {
+ int size = fields.size();
+ assert builders.size() == size;
+ JsonFieldInfo[] ordered = new JsonFieldInfo[size];
+ boolean[] selected = new boolean[size];
+ int outputIndex = 0;
+
+ if (propertyOrder != null) {
+ String[] names = propertyOrder.value();
+ if (names.length == 0 && !propertyOrder.alphabetic()) {
+ throw new ForyJsonException("Empty @JsonPropertyOrder on " +
type.getName());
+ }
+ for (String name : names) {
+ if (name.isEmpty()) {
+ throw new ForyJsonException("Empty @JsonPropertyOrder property on "
+ type.getName());
+ }
+ int propertyIndex = findOrderedProperty(name, builders, fields);
+ if (propertyIndex < 0) {
+ throw new ForyJsonException(
+ "Unknown @JsonPropertyOrder property " + name + " on " +
type.getName());
+ }
+ if (selected[propertyIndex]) {
+ throw new ForyJsonException(
+ "Duplicate @JsonPropertyOrder property " + name + " on " +
type.getName());
+ }
+ selected[propertyIndex] = true;
+ ordered[outputIndex++] = fields.get(propertyIndex);
+ }
+ }
+
+ int indexedCount = 0;
+ for (FieldBuilder builder : builders) {
+ if (builder.hasIndex()) {
+ indexedCount++;
+ }
+ }
+ if (indexedCount != 0) {
+ long[] indexed = new long[indexedCount];
+ int next = 0;
+ for (int i = 0; i < size; i++) {
+ int index = builders.get(i).explicitIndex;
+ if (index != JsonProperty.INDEX_UNKNOWN) {
+ indexed[next++] = ((long) index << 32) | (i & 0xffffffffL);
+ }
+ }
+ Arrays.sort(indexed);
+ for (int i = 1; i < indexedCount; i++) {
+ int previousIndex = (int) (indexed[i - 1] >>> 32);
+ int index = (int) (indexed[i] >>> 32);
+ if (previousIndex == index) {
+ int previousProperty = (int) indexed[i - 1];
+ int property = (int) indexed[i];
+ throw new ForyJsonException(
+ "Duplicate JSON property index "
+ + index
+ + " for "
+ + builders.get(previousProperty).name
+ + " from "
+ + builders.get(previousProperty).explicitIndexSource
+ + " and "
+ + builders.get(property).name
+ + " from "
+ + builders.get(property).explicitIndexSource
+ + " on "
+ + type.getName());
+ }
+ }
+ for (long indexedProperty : indexed) {
+ int propertyIndex = (int) indexedProperty;
+ if (!selected[propertyIndex]) {
+ selected[propertyIndex] = true;
+ ordered[outputIndex++] = fields.get(propertyIndex);
+ }
+ }
+ }
+
+ int unorderedStart = outputIndex;
+ for (int i = 0; i < size; i++) {
+ if (!selected[i]) {
+ ordered[outputIndex++] = fields.get(i);
+ }
+ }
+ if (propertyOrder != null && propertyOrder.alphabetic() && outputIndex -
unorderedStart > 1) {
+ Arrays.sort(
+ ordered,
+ unorderedStart,
+ outputIndex,
+ (left, right) -> left.name().compareTo(right.name()));
+ }
+ assert outputIndex == size;
+ return ordered;
+ }
+
+ private static int findOrderedProperty(
+ String name, List<FieldBuilder> builders, List<JsonFieldInfo> fields) {
+ for (int i = 0; i < fields.size(); i++) {
+ if (name.equals(fields.get(i).name())) {
+ return i;
+ }
+ }
+ for (int i = 0; i < builders.size(); i++) {
+ if (name.equals(builders.get(i).name)) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
private static void addFields(
Class<?> type,
boolean record,
@@ -235,7 +387,7 @@ public class ObjectCodec<T> implements JsonCodec<T>,
StringObjectWriter<T>, Utf8
}
FieldBuilder builder =
builders.computeIfAbsent(field.getName(), name -> new
FieldBuilder(name));
- builder.setField(field, write, read, write, readAllowed);
+ builder.setField(type, field, write, read, write, readAllowed);
}
}
}
@@ -252,7 +404,7 @@ public class ObjectCodec<T> implements JsonCodec<T>,
StringObjectWriter<T>, Utf8
builder = new FieldBuilder(propertyName);
builders.put(propertyName, builder);
}
- builder.setWriteGetter(method);
+ builder.setWriteGetter(type, method);
continue;
}
propertyName = setterPropertyName(method);
@@ -262,7 +414,7 @@ public class ObjectCodec<T> implements JsonCodec<T>,
StringObjectWriter<T>, Utf8
builder = new FieldBuilder(propertyName);
builders.put(propertyName, builder);
}
- builder.setReadSetter(method);
+ builder.setReadSetter(type, method);
}
}
}
@@ -278,7 +430,7 @@ public class ObjectCodec<T> implements JsonCodec<T>,
StringObjectWriter<T>, Utf8
// Record accessors carry component annotations in Java 16+, but field
access remains the
// optimized read/write owner. The accessor participates only in
logical-property annotation
// merging and is discarded before hot metadata is published.
- builder.mergeAnnotation(component.getAccessor());
+ builder.mergeAnnotation(type, component.getAccessor());
}
}
@@ -401,14 +553,28 @@ public class ObjectCodec<T> implements JsonCodec<T>,
StringObjectWriter<T>, Utf8
throw new ForyJsonException(
"@JsonCreator property is ignored for reading: " +
builder.name);
}
- builder.mergeAnnotation(parameters[i]);
+ builder.mergeAnnotation(type, parameters[i]);
if (property.include() != JsonProperty.Include.DEFAULT &&
!builder.hasWriteSource()) {
throw new ForyJsonException(
"Creator parameter inclusion requires a write source for " +
jsonName);
}
- } else if (property.include() != JsonProperty.Include.DEFAULT) {
- throw new ForyJsonException(
- "Creator-only property cannot declare an inclusion policy: " +
jsonName);
+ } else {
+ validatePropertyIndex(property.index(), jsonName, type,
parameters[i]);
+ if (property.index() != JsonProperty.INDEX_UNKNOWN) {
+ throw new ForyJsonException(
+ "Creator-only property "
+ + jsonName
+ + " cannot declare serialization index "
+ + property.index()
+ + " on "
+ + type.getName()
+ + " from "
+ + parameters[i]);
+ }
+ if (property.include() != JsonProperty.Include.DEFAULT) {
+ throw new ForyJsonException(
+ "Creator-only property cannot declare an inclusion policy: " +
jsonName);
+ }
}
Type resolved = ownerType.resolveType(parameterTypes[i]).getType();
fields[i] = new JsonCreatorFieldInfo(jsonName, i, resolved,
rawTypes[i]);
@@ -418,6 +584,21 @@ public class ObjectCodec<T> implements JsonCodec<T>,
StringObjectWriter<T>, Utf8
return new JsonCreatorInfo(type, creator, fields,
creatorDefaults(rawTypes));
}
+ private static void validatePropertyIndex(
+ int index, String propertyName, Class<?> type, AnnotatedElement source) {
+ if (index < JsonProperty.INDEX_UNKNOWN) {
+ throw new ForyJsonException(
+ "Invalid JSON property index "
+ + index
+ + " for property "
+ + propertyName
+ + " on "
+ + type.getName()
+ + " from "
+ + source);
+ }
+ }
+
private static void validateCreator(Class<?> type, Executable creator) {
int modifiers = creator.getModifiers();
if (!Modifier.isPublic(modifiers)
@@ -1043,6 +1224,8 @@ public class ObjectCodec<T> implements JsonCodec<T>,
StringObjectWriter<T>, Utf8
private JsonFieldAccessor readAccessor;
private String explicitName;
private AnnotatedElement explicitNameSource;
+ private int explicitIndex = JsonProperty.INDEX_UNKNOWN;
+ private AnnotatedElement explicitIndexSource;
private JsonProperty.Include explicitInclude =
JsonProperty.Include.DEFAULT;
private AnnotatedElement explicitIncludeSource;
private boolean creatorBound;
@@ -1052,6 +1235,7 @@ public class ObjectCodec<T> implements JsonCodec<T>,
StringObjectWriter<T>, Utf8
}
private void setField(
+ Class<?> type,
Field field,
boolean writeSource,
boolean readSink,
@@ -1069,11 +1253,11 @@ public class ObjectCodec<T> implements JsonCodec<T>,
StringObjectWriter<T>, Utf8
if (readSink) {
readField = field;
}
- mergeAnnotation(field);
+ mergeAnnotation(type, field);
}
- private void setWriteGetter(Method getter) {
- mergeAnnotation(getter);
+ private void setWriteGetter(Class<?> type, Method getter) {
+ mergeAnnotation(type, getter);
if (field != null && !fieldWriteAllowed) {
return;
}
@@ -1084,8 +1268,8 @@ public class ObjectCodec<T> implements JsonCodec<T>,
StringObjectWriter<T>, Utf8
writeField = null;
}
- private void setReadSetter(Method setter) {
- mergeAnnotation(setter);
+ private void setReadSetter(Class<?> type, Method setter) {
+ mergeAnnotation(type, setter);
if (field != null && !fieldReadAllowed) {
return;
}
@@ -1105,7 +1289,13 @@ public class ObjectCodec<T> implements JsonCodec<T>,
StringObjectWriter<T>, Utf8
}
private boolean hasConfiguration() {
- return explicitName != null || explicitInclude !=
JsonProperty.Include.DEFAULT;
+ return explicitName != null
+ || explicitIndex != JsonProperty.INDEX_UNKNOWN
+ || explicitInclude != JsonProperty.Include.DEFAULT;
+ }
+
+ private boolean hasIndex() {
+ return explicitIndex != JsonProperty.INDEX_UNKNOWN;
}
private boolean hasLogicalMember() {
@@ -1183,11 +1373,34 @@ public class ObjectCodec<T> implements JsonCodec<T>,
StringObjectWriter<T>, Utf8
ownerType);
}
- private void mergeAnnotation(AnnotatedElement source) {
+ private void mergeAnnotation(Class<?> type, AnnotatedElement source) {
JsonProperty property = source.getAnnotation(JsonProperty.class);
if (property == null) {
return;
}
+ int declaredIndex = property.index();
+ validatePropertyIndex(declaredIndex, name, type, source);
+ if (declaredIndex != JsonProperty.INDEX_UNKNOWN) {
+ if (explicitIndex != JsonProperty.INDEX_UNKNOWN && explicitIndex !=
declaredIndex) {
+ throw new ForyJsonException(
+ "Conflicting JSON property indexes for property "
+ + name
+ + " on "
+ + type.getName()
+ + ": "
+ + explicitIndex
+ + " from "
+ + explicitIndexSource
+ + " and "
+ + declaredIndex
+ + " from "
+ + source);
+ }
+ explicitIndex = declaredIndex;
+ if (explicitIndexSource == null) {
+ explicitIndexSource = source;
+ }
+ }
String declaredName = property.value();
if (!declaredName.isEmpty()) {
if (explicitName != null && !explicitName.equals(declaredName)) {
diff --git
a/java/fory-json/src/test/java/org/apache/fory/json/JsonPropertyOrderTest.java
b/java/fory-json/src/test/java/org/apache/fory/json/JsonPropertyOrderTest.java
new file mode 100644
index 000000000..4dea576e4
--- /dev/null
+++
b/java/fory-json/src/test/java/org/apache/fory/json/JsonPropertyOrderTest.java
@@ -0,0 +1,486 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.fory.json;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertThrows;
+import static org.testng.Assert.assertTrue;
+import static org.testng.Assert.expectThrows;
+
+import java.nio.charset.StandardCharsets;
+import org.apache.fory.json.annotation.JsonCreator;
+import org.apache.fory.json.annotation.JsonIgnore;
+import org.apache.fory.json.annotation.JsonProperty;
+import org.apache.fory.json.annotation.JsonPropertyOrder;
+import org.apache.fory.platform.JdkVersion;
+import org.testng.SkipException;
+import org.testng.annotations.Factory;
+import org.testng.annotations.Test;
+
+public class JsonPropertyOrderTest extends ForyJsonTestModels {
+ @Factory(dataProvider = "enableCodegen")
+ public JsonPropertyOrderTest(boolean codegen) {
+ super(codegen);
+ }
+
+ @Test
+ public void explicitAndIndexOrder() {
+ ForyJson json = newJson();
+ CombinedOrder value =
+ json.fromJson(
+
"{\"age\":4,\"name\":\"alice\",\"id\":1,\"email\":\"[email protected]\"}",
+ CombinedOrder.class);
+ assertEquals(value.id, 1);
+ assertEquals(value.name, "alice");
+ assertEquals(value.email, "[email protected]");
+ assertEquals(value.age, 4);
+ assertEquals(
+ json.toJson(value),
"{\"id\":1,\"email\":\"[email protected]\",\"name\":\"alice\",\"age\":4}");
+ assertEquals(
+ new String(json.toJsonBytes(value), StandardCharsets.UTF_8),
+
"{\"id\":1,\"email\":\"[email protected]\",\"name\":\"alice\",\"age\":4}");
+ assertGeneratedWhenSupported(json, CombinedOrder.class);
+ }
+
+ @Test
+ public void resolveOrderNames() {
+ ForyJson json =
+
newJsonBuilder().withPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE).build();
+ assertEquals(
+ json.toJson(new RenamedOrder()),
+
"{\"display_name\":\"display\",\"plain_name\":\"plain\",\"tail\":\"tail\"}");
+ assertEquals(json.toJson(new FinalNameWins()),
"{\"target\":1,\"other\":2}");
+ assertGeneratedWhenSupported(json, RenamedOrder.class);
+ assertGeneratedWhenSupported(json, FinalNameWins.class);
+ }
+
+ @Test
+ public void alphabeticOrder() {
+ ForyJson json = newJson();
+ AlphabeticOnly value = json.fromJson("{\"middle\":2,\"z\":1,\"a\":3}",
AlphabeticOnly.class);
+ assertEquals(value.z, 1);
+ assertEquals(value.middle, 2);
+ assertEquals(value.renamed, 3);
+ assertEquals(json.toJson(value), "{\"a\":3,\"middle\":2,\"z\":1}");
+ assertEquals(
+ new String(json.toJsonBytes(value), StandardCharsets.UTF_8),
+ "{\"a\":3,\"middle\":2,\"z\":1}");
+ assertGeneratedWhenSupported(json, AlphabeticOnly.class);
+
+ ForyJson snakeCase =
+
newJsonBuilder().withPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE).build();
+ assertEquals(snakeCase.toJson(new AlphabeticNaming()),
"{\"account_name\":2,\"zip_code\":1}");
+ assertGeneratedWhenSupported(snakeCase, AlphabeticNaming.class);
+ }
+
+ @Test
+ public void alphabeticPrecedence() {
+ ForyJson json = newJson();
+ assertEquals(
+ json.toJson(new CombinedAlphabeticOrder()),
+
"{\"id\":1,\"email\":\"email\",\"name\":\"name\",\"beta\":\"beta\",\"zeta\":\"zeta\"}");
+ assertGeneratedWhenSupported(json, CombinedAlphabeticOrder.class);
+ }
+
+ @Test
+ public void indexOrder() {
+ ForyJson json = newJson();
+ assertEquals(json.toJson(new IndexedOnly()), "{\"a\":1,\"c\":3,\"b\":2}");
+ assertEquals(json.toJson(new RepeatedIndex()), "{\"value\":3,\"tail\":4}");
+ assertGeneratedWhenSupported(json, IndexedOnly.class);
+ assertGeneratedWhenSupported(json, RepeatedIndex.class);
+ }
+
+ @Test
+ public void creatorIndex() {
+ ForyJson json = newJson();
+ CreatorOrder value = json.fromJson("{\"name\":\"alice\",\"id\":7}",
CreatorOrder.class);
+ assertEquals(value.id, 7);
+ assertEquals(value.name, "alice");
+ assertEquals(json.toJson(value), "{\"id\":7,\"name\":\"alice\"}");
+ assertGeneratedWhenSupported(json, CreatorOrder.class);
+ }
+
+ @Test
+ public void creatorAlphabetic() {
+ ForyJson json = newJson();
+ AlphabeticCreator value = json.fromJson("{\"a\":2,\"z\":1}",
AlphabeticCreator.class);
+ assertEquals(value.z, 1);
+ assertEquals(value.a, 2);
+ assertEquals(json.toJson(value), "{\"a\":2,\"z\":1}");
+ assertGeneratedWhenSupported(json, AlphabeticCreator.class);
+ }
+
+ @Test
+ public void recordIndex() throws Exception {
+ if (JdkVersion.MAJOR_VERSION < 17) {
+ throw new SkipException("Java record test requires JDK 17+");
+ }
+ Class<?> type =
+ compileRecordClass(
+ "JsonOrderedRecord",
+ "package org.apache.fory.json.records;\n"
+ + "import org.apache.fory.json.annotation.JsonProperty;\n"
+ + "public record JsonOrderedRecord(@JsonProperty(index = 1)
int b, "
+ + "@JsonProperty(index = 0) int a) {}\n");
+ Object value = jsonRecord(type, 2, 1);
+ ForyJson json = newJson();
+ assertEquals(json.toJson(value), "{\"a\":1,\"b\":2}");
+ Object decoded = json.fromJson("{\"b\":4,\"a\":3}", type);
+ assertEquals(type.getMethod("a").invoke(decoded), Integer.valueOf(3));
+ assertEquals(type.getMethod("b").invoke(decoded), Integer.valueOf(4));
+ assertGeneratedWhenSupported(json, type);
+ }
+
+ @Test
+ public void recordAlphabetic() throws Exception {
+ if (JdkVersion.MAJOR_VERSION < 17) {
+ throw new SkipException("Java record test requires JDK 17+");
+ }
+ Class<?> type =
+ compileRecordClass(
+ "JsonAlphabeticRecord",
+ "package org.apache.fory.json.records;\n"
+ + "import org.apache.fory.json.annotation.JsonPropertyOrder;\n"
+ + "@JsonPropertyOrder(alphabetic = true)\n"
+ + "public record JsonAlphabeticRecord(int z, int a) {}\n");
+ Object value = type.getConstructor(int.class, int.class).newInstance(1, 2);
+ ForyJson json = newJson();
+ assertEquals(json.toJson(value), "{\"a\":2,\"z\":1}");
+ Object decoded = json.fromJson("{\"z\":3,\"a\":4}", type);
+ assertEquals(type.getMethod("z").invoke(decoded), Integer.valueOf(3));
+ assertEquals(type.getMethod("a").invoke(decoded), Integer.valueOf(4));
+ assertGeneratedWhenSupported(json, type);
+ }
+
+ private static Object jsonRecord(Class<?> type, int b, int a) throws
Exception {
+ return type.getConstructor(int.class, int.class).newInstance(b, a);
+ }
+
+ @Test
+ public void superclassOrder() {
+ ForyJson json = newJson();
+ assertEquals(json.toJson(new WholeChild()), "{\"c\":3,\"a\":1,\"b\":2}");
+ assertEquals(json.toJson(new InheritedChild()),
"{\"child\":3,\"a\":1,\"b\":2}");
+ assertEquals(json.toJson(new OverrideInvalidChild()), "{\"b\":2,\"a\":1}");
+ assertEquals(json.toJson(new InterfaceOrderImpl()), "{\"a\":1,\"b\":2}");
+ assertEquals(json.toJson(new InheritedAlphabeticChild()),
"{\"a\":1,\"b\":2,\"c\":3}");
+ assertEquals(json.toJson(new AlphabeticOverrideChild()),
"{\"a\":1,\"b\":2}");
+ assertGeneratedWhenSupported(json, WholeChild.class);
+ assertGeneratedWhenSupported(json, InheritedChild.class);
+ assertGeneratedWhenSupported(json, OverrideInvalidChild.class);
+ assertGeneratedWhenSupported(json, InterfaceOrderImpl.class);
+ assertGeneratedWhenSupported(json, InheritedAlphabeticChild.class);
+ assertGeneratedWhenSupported(json, AlphabeticOverrideChild.class);
+ }
+
+ @Test
+ public void rejectInvalidIndex() {
+ ForyJson json = newJson();
+ assertThrows(ForyJsonException.class, () -> json.toJson(new
NegativeIndex()));
+ assertThrows(ForyJsonException.class, () -> json.toJson(new
DuplicateIndex()));
+ ForyJsonException conflict =
+ expectThrows(ForyJsonException.class, () -> json.toJson(new
ConflictingIndex()));
+ assertTrue(
+ conflict.getMessage().contains("property value on " +
ConflictingIndex.class.getName()));
+ assertThrows(ForyJsonException.class, () -> json.toJson(new
SetterOnlyIndex()));
+ assertThrows(ForyJsonException.class, () -> json.toJson(new
WriteIgnoredIndex()));
+ assertThrows(
+ ForyJsonException.class, () -> json.fromJson("{\"input\":1}",
CreatorOnlyIndex.class));
+ ForyJsonException error =
+ expectThrows(
+ ForyJsonException.class, () -> json.fromJson("{\"id\":1}",
NegativeCreatorIndex.class));
+ assertTrue(
+ error.getMessage().contains("property id on " +
NegativeCreatorIndex.class.getName()));
+ }
+
+ @Test
+ public void rejectInvalidOrder() {
+ ForyJson json = newJson();
+ assertThrows(ForyJsonException.class, () -> json.toJson(new EmptyOrder()));
+ assertThrows(ForyJsonException.class, () -> json.toJson(new
EmptyOrderEntry()));
+ assertThrows(ForyJsonException.class, () -> json.toJson(new
UnknownOrder()));
+ assertThrows(ForyJsonException.class, () -> json.toJson(new
DuplicateOrderEntry()));
+ }
+
+ @JsonPropertyOrder({"id"})
+ public static final class CombinedOrder {
+ @JsonProperty(index = 20)
+ public String name;
+
+ @JsonProperty(index = 10)
+ public String email;
+
+ public int id;
+ public int age;
+ }
+
+ @JsonPropertyOrder({"display_name", "plainName"})
+ public static final class RenamedOrder {
+ public String plainName = "plain";
+
+ @JsonProperty("display_name")
+ public String displayName = "display";
+
+ public String tail = "tail";
+ }
+
+ @JsonPropertyOrder({"target"})
+ public static final class FinalNameWins {
+ @JsonProperty("other")
+ public int target = 2;
+
+ @JsonProperty("target")
+ public int renamed = 1;
+ }
+
+ @JsonPropertyOrder(alphabetic = true)
+ public static final class AlphabeticOnly {
+ public int z;
+ public int middle;
+
+ @JsonProperty("a")
+ public int renamed;
+ }
+
+ @JsonPropertyOrder(alphabetic = true)
+ public static final class AlphabeticNaming {
+ public int zipCode = 1;
+ public int accountName = 2;
+ }
+
+ @JsonPropertyOrder(
+ value = {"id"},
+ alphabetic = true)
+ public static final class CombinedAlphabeticOrder {
+ public String zeta = "zeta";
+
+ @JsonProperty(index = 20)
+ public String name = "name";
+
+ public int id = 1;
+
+ @JsonProperty(index = 10)
+ public String email = "email";
+
+ public String beta = "beta";
+ }
+
+ public static final class IndexedOnly {
+ @JsonProperty(index = 2)
+ public int c = 3;
+
+ public int b = 2;
+
+ @JsonProperty(index = 0)
+ public int a = 1;
+ }
+
+ public static final class RepeatedIndex {
+ @JsonProperty(index = 0)
+ private int value = 3;
+
+ public int tail = 4;
+
+ @JsonProperty(index = 0)
+ public int getValue() {
+ return value;
+ }
+
+ @JsonProperty(index = 0)
+ public void setValue(int value) {
+ this.value = value;
+ }
+ }
+
+ public static final class CreatorOrder {
+ public final String name;
+ public final int id;
+
+ private CreatorOrder(int id, String name) {
+ this.id = id;
+ this.name = name;
+ }
+
+ @JsonCreator
+ public static CreatorOrder create(
+ @JsonProperty(value = "id", index = 0) int id,
+ @JsonProperty(value = "name", index = 1) String name) {
+ return new CreatorOrder(id, name);
+ }
+ }
+
+ @JsonPropertyOrder(alphabetic = true)
+ public static final class AlphabeticCreator {
+ public final int z;
+ public final int a;
+
+ private AlphabeticCreator(int z, int a) {
+ this.z = z;
+ this.a = a;
+ }
+
+ @JsonCreator
+ public static AlphabeticCreator create(@JsonProperty("z") int z,
@JsonProperty("a") int a) {
+ return new AlphabeticCreator(z, a);
+ }
+ }
+
+ @JsonPropertyOrder({"b"})
+ public static class WholeParent {
+ @JsonProperty(index = 1)
+ public int b = 2;
+
+ @JsonProperty(index = 0)
+ public int a = 1;
+ }
+
+ @JsonPropertyOrder({"c"})
+ public static final class WholeChild extends WholeParent {
+ public int c = 3;
+ }
+
+ @JsonPropertyOrder({"child", "a"})
+ public static class InheritedParent {
+ public int b = 2;
+ public int a = 1;
+ }
+
+ public static final class InheritedChild extends InheritedParent {
+ public int child = 3;
+ }
+
+ @JsonPropertyOrder({"missing"})
+ public static class InvalidOrderParent {
+ public int a = 1;
+ }
+
+ @JsonPropertyOrder({"b"})
+ public static final class OverrideInvalidChild extends InvalidOrderParent {
+ public int b = 2;
+ }
+
+ @JsonPropertyOrder({"b"})
+ public interface InterfaceOrder {}
+
+ public static final class InterfaceOrderImpl implements InterfaceOrder {
+ public int b = 2;
+
+ @JsonProperty(index = 0)
+ public int a = 1;
+ }
+
+ @JsonPropertyOrder(alphabetic = true)
+ public static class AlphabeticParent {
+ public int b = 2;
+ public int a = 1;
+ }
+
+ public static final class InheritedAlphabeticChild extends AlphabeticParent {
+ public int c = 3;
+ }
+
+ @JsonPropertyOrder(alphabetic = true)
+ public static final class AlphabeticOverrideChild extends InvalidOrderParent
{
+ public int b = 2;
+ }
+
+ public static final class NegativeIndex {
+ @JsonProperty(index = -2)
+ public int value;
+ }
+
+ @JsonPropertyOrder({"a"})
+ public static final class DuplicateIndex {
+ @JsonProperty(index = 0)
+ public int a;
+
+ @JsonProperty(index = 0)
+ public int b;
+ }
+
+ public static final class ConflictingIndex {
+ @JsonProperty(index = 0)
+ private int value;
+
+ @JsonProperty(index = 1)
+ public int getValue() {
+ return value;
+ }
+ }
+
+ public static final class SetterOnlyIndex {
+ @JsonProperty(index = 0)
+ public void setValue(int value) {}
+ }
+
+ public static final class WriteIgnoredIndex {
+ @JsonIgnore(ignoreRead = false, ignoreWrite = true)
+ @JsonProperty(index = 0)
+ public int value;
+ }
+
+ public static final class CreatorOnlyIndex {
+ public final int id;
+
+ private CreatorOnlyIndex(int id) {
+ this.id = id;
+ }
+
+ @JsonCreator
+ public static CreatorOnlyIndex create(@JsonProperty(value = "input", index
= 0) int id) {
+ return new CreatorOnlyIndex(id);
+ }
+ }
+
+ public static final class NegativeCreatorIndex {
+ public final int id;
+
+ private NegativeCreatorIndex(int id) {
+ this.id = id;
+ }
+
+ @JsonCreator
+ public static NegativeCreatorIndex create(@JsonProperty(value = "id",
index = -2) int id) {
+ return new NegativeCreatorIndex(id);
+ }
+ }
+
+ @JsonPropertyOrder({})
+ public static final class EmptyOrder {
+ public int value;
+ }
+
+ @JsonPropertyOrder({""})
+ public static final class EmptyOrderEntry {
+ public int value;
+ }
+
+ @JsonPropertyOrder({"missing"})
+ public static final class UnknownOrder {
+ public int value;
+ }
+
+ @JsonPropertyOrder({"renamed", "value"})
+ public static final class DuplicateOrderEntry {
+ @JsonProperty("renamed")
+ public int value;
+ }
+}
diff --git
a/java/fory-json/src/test/java/org/apache/fory/json/JsonSubTypesTest.java
b/java/fory-json/src/test/java/org/apache/fory/json/JsonSubTypesTest.java
index 3c995c975..99713359e 100644
--- a/java/fory-json/src/test/java/org/apache/fory/json/JsonSubTypesTest.java
+++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonSubTypesTest.java
@@ -31,6 +31,7 @@ import java.io.ByteArrayOutputStream;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.List;
+import org.apache.fory.json.annotation.JsonPropertyOrder;
import org.apache.fory.json.annotation.JsonSubTypes;
import org.apache.fory.json.annotation.JsonSubTypes.Inclusion;
import org.apache.fory.json.codec.JsonCodec;
@@ -63,19 +64,19 @@ public class JsonSubTypesTest extends ForyJsonTestModels {
Shape decoded = json.fromJson(text, Shape.class);
assertEquals(((Circle) decoded).radius, 2);
assertEquals(new String(json.toJsonBytes(value, Shape.class),
StandardCharsets.UTF_8), text);
- assertInlineWriterCapabilities(json);
+ assertInlineWriterCapabilities(json, Circle.class);
Shape utf16 =
json.fromJson("{\"说明\":\"值\",\"radius\":3,\"kind\":\"circle\"}", Shape.class);
assertEquals(((Circle) utf16).radius, 3);
Shape utf8 = json.fromJson(text.getBytes(StandardCharsets.UTF_8),
Shape.class);
assertEquals(((Circle) utf8).radius, 2);
}
- private void assertInlineWriterCapabilities(ForyJson json) {
+ private void assertInlineWriterCapabilities(ForyJson json, Class<?> type) {
JsonTypeResolver resolver = JsonTestSupport.primaryTypeResolver(json);
resolver.lockJIT();
try {
- JsonTypeInfo info = resolver.getTypeInfo(Circle.class, Circle.class);
- ObjectCodec<Circle> owner = resolver.getObjectCodec(Circle.class);
+ JsonTypeInfo info = resolver.getTypeInfo(type, type);
+ ObjectCodec<?> owner = resolver.getObjectCodec(type);
if (codegenEnabled()) {
assertNotSame(info.stringWriter(), owner);
assertNotSame(info.utf8Writer(), owner);
@@ -94,6 +95,8 @@ public class JsonSubTypesTest extends ForyJsonTestModels {
Shape value = new Rectangle(3, 4);
String text = json.toJson(value, Shape.class);
assertEquals(text, "{\"kind\":\"rectangle\",\"height\":4,\"width\":3}");
+ assertEquals(new String(json.toJsonBytes(value, Shape.class),
StandardCharsets.UTF_8), text);
+ assertInlineWriterCapabilities(json, Rectangle.class);
Shape decoded = json.fromJson(text, Shape.class);
assertEquals(((Rectangle) decoded).width, 3);
assertEquals(((Rectangle) decoded).height, 4);
@@ -343,9 +346,10 @@ public class JsonSubTypesTest extends ForyJsonTestModels {
}
}
+ @JsonPropertyOrder(alphabetic = true)
public static final class Rectangle implements Shape {
- public int height;
public int width;
+ public int height;
public Rectangle() {}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]