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]

Reply via email to