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 37e934c99 feat(java): add abstract json value codec (#3865)
37e934c99 is described below

commit 37e934c99e8ac5679b1fd66473fda4d647525787
Author: Shawn Yang <[email protected]>
AuthorDate: Thu Jul 16 18:05:18 2026 +0530

    feat(java): add abstract json value codec (#3865)
    
    ## 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                    |  41 +++-----
 java/fory-json/README.md                           |  41 +++-----
 .../fory/json/codec/AbstractJsonValueCodec.java    |  76 ++++++++++++++
 .../org/apache/fory/json/codec/JsonValueCodec.java |   3 +
 .../org/apache/fory/json/reader/JsonReader.java    |   9 ++
 .../apache/fory/json/reader/Latin1JsonReader.java  |   1 +
 .../apache/fory/json/reader/Utf16JsonReader.java   |   1 +
 .../apache/fory/json/reader/Utf8JsonReader.java    |   1 +
 .../fory/json/AbstractJsonValueCodecTest.java      | 111 +++++++++++++++++++++
 9 files changed, 226 insertions(+), 58 deletions(-)

diff --git a/docs/guide/java/json-support.md b/docs/guide/java/json-support.md
index 6d3dbfe44..de5331913 100644
--- a/docs/guide/java/json-support.md
+++ b/docs/guide/java/json-support.md
@@ -895,29 +895,18 @@ concrete String/UTF-8 writers and Latin-1/UTF-16/UTF-8 
readers. It is a direct s
 not a JSON abstract syntax tree (AST) codec. It never handles Map keys; 
`MapKeyCodec` owns JSON
 object member names.
 
-Implement all five representations with the same JSON shape:
+For an application codec with the same semantics in every representation, 
extend
+`AbstractJsonValueCodec<T>` and implement the JSON shape once:
 
 ```java
 import java.math.BigDecimal;
-import org.apache.fory.json.codec.JsonValueCodec;
-import org.apache.fory.json.reader.Latin1JsonReader;
-import org.apache.fory.json.reader.Utf16JsonReader;
-import org.apache.fory.json.reader.Utf8JsonReader;
-import org.apache.fory.json.writer.StringJsonWriter;
-import org.apache.fory.json.writer.Utf8JsonWriter;
-
-public final class MoneyCodec implements JsonValueCodec<Money> {
-  @Override
-  public void writeString(StringJsonWriter writer, Money value) {
-    if (value == null) {
-      writer.writeNull();
-    } else {
-      writer.writeBigDecimal(value.amount);
-    }
-  }
+import org.apache.fory.json.codec.AbstractJsonValueCodec;
+import org.apache.fory.json.reader.JsonReader;
+import org.apache.fory.json.writer.JsonWriter;
 
+public final class MoneyCodec extends AbstractJsonValueCodec<Money> {
   @Override
-  public void writeUtf8(Utf8JsonWriter writer, Money value) {
+  public void write(JsonWriter writer, Money value) {
     if (value == null) {
       writer.writeNull();
     } else {
@@ -926,17 +915,7 @@ public final class MoneyCodec implements 
JsonValueCodec<Money> {
   }
 
   @Override
-  public Money readLatin1(Latin1JsonReader reader) {
-    return reader.tryReadNullToken() ? null : new 
Money(reader.readBigDecimal());
-  }
-
-  @Override
-  public Money readUtf16(Utf16JsonReader reader) {
-    return reader.tryReadNullToken() ? null : new 
Money(reader.readBigDecimal());
-  }
-
-  @Override
-  public Money readUtf8(Utf8JsonReader reader) {
+  public Money read(JsonReader reader) {
     return reader.tryReadNullToken() ? null : new 
Money(reader.readBigDecimal());
   }
 }
@@ -950,6 +929,10 @@ final class Money {
 }
 ```
 
+`AbstractJsonValueCodec` adds one virtual method call per operation. For a
+performance-sensitive codec, or when behavior depends on a concrete reader or 
writer, implement
+`JsonValueCodec<T>` directly and provide all five representation-specific 
methods.
+
 Register it once:
 
 ```java
diff --git a/java/fory-json/README.md b/java/fory-json/README.md
index e13e9d33f..8380b7545 100644
--- a/java/fory-json/README.md
+++ b/java/fory-json/README.md
@@ -995,29 +995,18 @@ reader. It is not a JSON abstract syntax tree (AST) or 
`JsonNode` codec. It owns
 including JSON null, but never handles a Map key; `MapKeyCodec` remains 
responsible for JSON object
 member names.
 
-Implement all five representations with the same JSON shape:
+For an application codec with the same semantics in every representation, 
extend
+`AbstractJsonValueCodec<T>` and implement the JSON shape once:
 
 ```java
 import java.math.BigDecimal;
-import org.apache.fory.json.codec.JsonValueCodec;
-import org.apache.fory.json.reader.Latin1JsonReader;
-import org.apache.fory.json.reader.Utf16JsonReader;
-import org.apache.fory.json.reader.Utf8JsonReader;
-import org.apache.fory.json.writer.StringJsonWriter;
-import org.apache.fory.json.writer.Utf8JsonWriter;
-
-public final class MoneyCodec implements JsonValueCodec<Money> {
-  @Override
-  public void writeString(StringJsonWriter writer, Money value) {
-    if (value == null) {
-      writer.writeNull();
-    } else {
-      writer.writeBigDecimal(value.amount);
-    }
-  }
+import org.apache.fory.json.codec.AbstractJsonValueCodec;
+import org.apache.fory.json.reader.JsonReader;
+import org.apache.fory.json.writer.JsonWriter;
 
+public final class MoneyCodec extends AbstractJsonValueCodec<Money> {
   @Override
-  public void writeUtf8(Utf8JsonWriter writer, Money value) {
+  public void write(JsonWriter writer, Money value) {
     if (value == null) {
       writer.writeNull();
     } else {
@@ -1026,17 +1015,7 @@ public final class MoneyCodec implements 
JsonValueCodec<Money> {
   }
 
   @Override
-  public Money readLatin1(Latin1JsonReader reader) {
-    return reader.tryReadNullToken() ? null : new 
Money(reader.readBigDecimal());
-  }
-
-  @Override
-  public Money readUtf16(Utf16JsonReader reader) {
-    return reader.tryReadNullToken() ? null : new 
Money(reader.readBigDecimal());
-  }
-
-  @Override
-  public Money readUtf8(Utf8JsonReader reader) {
+  public Money read(JsonReader reader) {
     return reader.tryReadNullToken() ? null : new 
Money(reader.readBigDecimal());
   }
 }
@@ -1050,6 +1029,10 @@ final class Money {
 }
 ```
 
+`AbstractJsonValueCodec` adds one virtual method call per operation. For a
+performance-sensitive codec, or when behavior depends on a concrete reader or 
writer, implement
+`JsonValueCodec<T>` directly and provide all five representation-specific 
methods.
+
 ```java
 import org.apache.fory.json.ForyJson;
 
diff --git 
a/java/fory-json/src/main/java/org/apache/fory/json/codec/AbstractJsonValueCodec.java
 
b/java/fory-json/src/main/java/org/apache/fory/json/codec/AbstractJsonValueCodec.java
new file mode 100644
index 000000000..c5a45cf6d
--- /dev/null
+++ 
b/java/fory-json/src/main/java/org/apache/fory/json/codec/AbstractJsonValueCodec.java
@@ -0,0 +1,76 @@
+/*
+ * 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.codec;
+
+import org.apache.fory.json.reader.JsonReader;
+import org.apache.fory.json.reader.Latin1JsonReader;
+import org.apache.fory.json.reader.Utf16JsonReader;
+import org.apache.fory.json.reader.Utf8JsonReader;
+import org.apache.fory.json.writer.JsonWriter;
+import org.apache.fory.json.writer.StringJsonWriter;
+import org.apache.fory.json.writer.Utf8JsonWriter;
+
+/**
+ * Convenience base class for application-defined, representation-neutral JSON 
value codecs.
+ *
+ * <p>Subclasses implement one {@link #write(JsonWriter, Object)} method and 
one {@link
+ * #read(JsonReader)} method instead of repeating the same logic for every 
concrete reader and
+ * writer. Each operation owns one complete JSON value, including {@code 
null}, just like {@link
+ * JsonValueCodec}.
+ *
+ * <p>This class is intended only for user codecs. Its representation-neutral 
bridge adds a virtual
+ * method call to every read and write. Fory's built-in codecs must implement 
{@link JsonValueCodec}
+ * directly, and performance-sensitive user codecs should do the same. Direct 
implementation also
+ * remains necessary when a codec needs representation-specific reader or 
writer behavior.
+ *
+ * @param <T> Java value type encoded by this codec
+ */
+public abstract class AbstractJsonValueCodec<T> implements JsonValueCodec<T> {
+  /** Writes one complete JSON value, including {@code null}. */
+  public abstract void write(JsonWriter writer, T value);
+
+  /** Reads one complete JSON value, including JSON {@code null}. */
+  public abstract T read(JsonReader reader);
+
+  @Override
+  public final void writeString(StringJsonWriter writer, T value) {
+    write(writer, value);
+  }
+
+  @Override
+  public final void writeUtf8(Utf8JsonWriter writer, T value) {
+    write(writer, value);
+  }
+
+  @Override
+  public final T readLatin1(Latin1JsonReader reader) {
+    return read(reader);
+  }
+
+  @Override
+  public final T readUtf16(Utf16JsonReader reader) {
+    return read(reader);
+  }
+
+  @Override
+  public final T readUtf8(Utf8JsonReader reader) {
+    return read(reader);
+  }
+}
diff --git 
a/java/fory-json/src/main/java/org/apache/fory/json/codec/JsonValueCodec.java 
b/java/fory-json/src/main/java/org/apache/fory/json/codec/JsonValueCodec.java
index 08f93d46c..8accaa559 100644
--- 
a/java/fory-json/src/main/java/org/apache/fory/json/codec/JsonValueCodec.java
+++ 
b/java/fory-json/src/main/java/org/apache/fory/json/codec/JsonValueCodec.java
@@ -34,6 +34,9 @@ package org.apache.fory.json.codec;
  * the Java type's semantics for every representation. Generated object 
specializations implement
  * only the narrow capability they accelerate and are installed independently 
in the corresponding
  * {@link org.apache.fory.json.resolver.JsonTypeInfo} slot.
+ *
+ * <p>Application codecs whose semantics are representation-neutral may extend 
{@link
+ * AbstractJsonValueCodec}. Performance-sensitive codecs should implement this 
interface directly.
  */
 public interface JsonValueCodec<T>
     extends StringWriterCodec<T>,
diff --git 
a/java/fory-json/src/main/java/org/apache/fory/json/reader/JsonReader.java 
b/java/fory-json/src/main/java/org/apache/fory/json/reader/JsonReader.java
index 0e27da04a..6cb40fc72 100644
--- a/java/fory-json/src/main/java/org/apache/fory/json/reader/JsonReader.java
+++ b/java/fory-json/src/main/java/org/apache/fory/json/reader/JsonReader.java
@@ -515,6 +515,15 @@ public abstract class JsonReader {
     return false;
   }
 
+  /**
+   * Tries to read a JSON {@code null} token.
+   *
+   * <p>Concrete readers override this method with their direct 
representation-specific token scan.
+   */
+  public boolean tryReadNullToken() {
+    return tryReadNull();
+  }
+
   public final boolean readBoolean() {
     skipWhitespace();
     if (startsWith("true")) {
diff --git 
a/java/fory-json/src/main/java/org/apache/fory/json/reader/Latin1JsonReader.java
 
b/java/fory-json/src/main/java/org/apache/fory/json/reader/Latin1JsonReader.java
index 23895d0c1..f2a81c518 100644
--- 
a/java/fory-json/src/main/java/org/apache/fory/json/reader/Latin1JsonReader.java
+++ 
b/java/fory-json/src/main/java/org/apache/fory/json/reader/Latin1JsonReader.java
@@ -429,6 +429,7 @@ public final class Latin1JsonReader extends JsonReader {
     throw error("Expected ',' or ']'");
   }
 
+  @Override
   public boolean tryReadNullToken() {
     skipWhitespaceFast();
     return tryReadNullLiteral();
diff --git 
a/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf16JsonReader.java 
b/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf16JsonReader.java
index 65f18223b..a5c896127 100644
--- 
a/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf16JsonReader.java
+++ 
b/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf16JsonReader.java
@@ -476,6 +476,7 @@ public final class Utf16JsonReader extends JsonReader {
     throw error("Expected ',' or ']'");
   }
 
+  @Override
   public boolean tryReadNullToken() {
     skipWhitespaceFast();
     return tryReadNullLiteral();
diff --git 
a/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf8JsonReader.java 
b/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf8JsonReader.java
index 3aaa77900..6e5240cb7 100644
--- 
a/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf8JsonReader.java
+++ 
b/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf8JsonReader.java
@@ -496,6 +496,7 @@ public final class Utf8JsonReader extends JsonReader {
     throw error("Expected ',' or ']'");
   }
 
+  @Override
   public boolean tryReadNullToken() {
     skipWhitespaceFast();
     return tryReadNullLiteral();
diff --git 
a/java/fory-json/src/test/java/org/apache/fory/json/AbstractJsonValueCodecTest.java
 
b/java/fory-json/src/test/java/org/apache/fory/json/AbstractJsonValueCodecTest.java
new file mode 100644
index 000000000..58a0b6fbf
--- /dev/null
+++ 
b/java/fory-json/src/test/java/org/apache/fory/json/AbstractJsonValueCodecTest.java
@@ -0,0 +1,111 @@
+/*
+ * 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.assertNull;
+
+import java.nio.charset.StandardCharsets;
+import org.apache.fory.json.codec.AbstractJsonValueCodec;
+import org.apache.fory.json.reader.JsonReader;
+import org.apache.fory.json.writer.JsonWriter;
+import org.apache.fory.json.writer.StringJsonWriter;
+import org.apache.fory.json.writer.Utf8JsonWriter;
+import org.testng.annotations.Test;
+
+public class AbstractJsonValueCodecTest {
+  @Test
+  public void bridges() {
+    TextValueCodec codec = new TextValueCodec();
+    TextValue value = new TextValue("你好");
+
+    StringJsonWriter stringWriter = JsonTestSupport.newStringWriter();
+    codec.writeString(stringWriter, value);
+    assertEquals(stringWriter.toJson(), "\"你好\"");
+
+    Utf8JsonWriter utf8Writer = JsonTestSupport.newUtf8Writer();
+    codec.writeUtf8(utf8Writer, value);
+    assertEquals(new String(utf8Writer.toJsonBytes(), StandardCharsets.UTF_8), 
"\"你好\"");
+
+    assertEquals(
+        codec.readLatin1(
+                
JsonTestSupport.newLatin1Reader("\"latin1\"".getBytes(StandardCharsets.ISO_8859_1)))
+            .text,
+        "latin1");
+    
assertEquals(codec.readUtf16(JsonTestSupport.newUtf16Reader("\"你好\"")).text, 
"你好");
+    assertEquals(
+        
codec.readUtf8(JsonTestSupport.newUtf8Reader("\"utf8\"".getBytes(StandardCharsets.UTF_8)))
+            .text,
+        "utf8");
+  }
+
+  @Test
+  public void representations() {
+    ForyJson json = newJson();
+    TextValue value = new TextValue("你好");
+    assertEquals(json.toJson(value), "\"你好\"");
+    assertEquals(new String(json.toJsonBytes(value), StandardCharsets.UTF_8), 
"\"你好\"");
+    assertEquals(json.fromJson("\"latin1\"", TextValue.class).text, "latin1");
+    assertEquals(json.fromJson("\"你好\"", TextValue.class).text, "你好");
+    assertEquals(
+        json.fromJson("\"utf8\"".getBytes(StandardCharsets.UTF_8), 
TextValue.class).text, "utf8");
+  }
+
+  @Test
+  public void nullValue() {
+    ForyJson json = newJson();
+    assertEquals(json.toJson(null, TextValue.class), "null");
+    assertEquals(
+        new String(json.toJsonBytes(null, TextValue.class), 
StandardCharsets.UTF_8), "null");
+    assertNull(json.fromJson("null", TextValue.class));
+    assertNull(json.fromJson("null".getBytes(StandardCharsets.UTF_8), 
TextValue.class));
+  }
+
+  private static ForyJson newJson() {
+    return ForyJson.builder()
+        .registerCodec(TextValue.class, new TextValueCodec())
+        .withAsyncCompilation(false)
+        .build();
+  }
+
+  private static final class TextValueCodec extends 
AbstractJsonValueCodec<TextValue> {
+    @Override
+    public void write(JsonWriter writer, TextValue value) {
+      if (value == null) {
+        writer.writeNull();
+      } else {
+        writer.writeString(value.text);
+      }
+    }
+
+    @Override
+    public TextValue read(JsonReader reader) {
+      return reader.tryReadNullToken() ? null : new 
TextValue(reader.readString());
+    }
+  }
+
+  private static final class TextValue {
+    private final String text;
+
+    private TextValue(String text) {
+      this.text = text;
+    }
+  }
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to