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]