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 379067682 feat(json): encode byte arrays as base64 JSON strings by 
default (#4012)
379067682 is described below

commit 379067682d58ca80a27d6cefdfcc43234d32ec8d
Author: Ingo Kegel <[email protected]>
AuthorDate: Tue Sep 1 04:24:25 2026 +0200

    feat(json): encode byte arrays as base64 JSON strings by default (#4012)
    
    ## Why?
    
    Closes #4011.
    
    JSON has no binary type. The ecosystem standard for byte arrays in JSON
    is a base64 string (see RFC 7493, the protobuf JSON mapping, Jackson,
    Gson, Moshi, kotlinx.serialization). Fory JSON currently writes byte[]
    as a JSON array of decimal numbers, which is about 2.1x larger on the
    wire and about 4.5x slower to write and read on binary-heavy payloads.
    
    ## What does this PR do?
    
    - Makes Base64ByteArrayCodec the default codec for byte[], previously
    opt-in via @JsonBase64. The annotation still works and is now redundant.
    - Speeds up Base64 reading: a single table-driven validation scan and a
    table-driven quad decode replace the two-pass decode.
    - readBase64 now reserves the decoded array in the graph memory budget,
    like other array reads.
    - Tests updated to the base64 default
    
    ## Related issues
    
    Closes #4011
    
    ## AI Contribution Checklist
    
    - [ no] 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?
    - [x ] Does this PR introduce any binary protocol compatibility change?
    
    ## Benchmark
    
    Round trip on a realistic payload with ~5 KB byte[] thumbnails, 2M
    iterations, JDK 25, Linux x86_64, project:
    https://github.com/ej-technologies/serialization-comparison
    
    |                                    | ns/op | avg bytes |
    | ----------------------- | -------: | ---------: |
    | Fory JSON 1.7.0          | 75936 | 20999      |
    | this PR                        | 16769 | 9742        |
    | Jackson JSON             | 27036 | 9814        |
    
    ---------
    
    Co-authored-by: chaokunyang <[email protected]>
---
 .agents/languages/java.md                          |   5 +
 docs/json/android.md                               |   6 +-
 docs/json/annotations.md                           |  36 +++---
 docs/json/graalvm.md                               |   6 +-
 docs/json/kotlin.md                                |   2 +-
 .../org/apache/fory/graalvm/ForyJsonExample.java   |  25 ++++-
 .../kotlin/json/corpus/PlatformCorpusChecks.kt     |   6 +
 .../kotlin/json/corpus/PlatformModels.kt           |   6 +
 .../kotlin/json/corpus/KspRetentionResourceTest.kt |   9 +-
 .../processing/JsonMixinAnnotations.java           |   4 +-
 .../annotation/processing/JsonTypeProcessor.java   |  13 ++-
 .../processing/JsonTypeProcessorTest.java          |  19 ++--
 .../{JsonBase64.java => JsonByteArray.java}        |  23 +++-
 .../org/apache/fory/json/annotation/JsonMixin.java |   2 +-
 .../org/apache/fory/json/codec/ArrayCodec.java     |  18 +--
 .../fory/json/codec/Base64ByteArrayCodec.java      |   2 +
 .../apache/fory/json/codec/ObjectCodecBuilder.java |  62 ++++++-----
 .../org/apache/fory/json/reader/JsonReader.java    | 104 +++++++++++++----
 .../fory/json/resolver/JsonMixinAnnotations.java   |   4 +-
 .../fory/json/resolver/JsonValueDeclaration.java   |   6 +-
 .../apache/fory/json/ForyJsonGraalVMFeature.java   |  11 +-
 .../apache/fory/json/JsonAndroidRuntimeTest.java   |   6 +-
 ...nTest.java => JsonByteArrayAnnotationTest.java} | 124 +++++++++++++++++----
 .../org/apache/fory/json/JsonContainerTest.java    |  24 +++-
 .../fory/json/JsonGraphMemoryBudgetTest.java       |  33 +++++-
 .../java/org/apache/fory/json/JsonMixinTest.java   |  16 ++-
 .../java/org/apache/fory/json/JsonScalarTest.java  |   2 +-
 .../org/apache/fory/json/JsonUnwrappedTest.java    |   6 +-
 .../apache/fory/json/kotlin/ksp/KspModelBuilder.kt |  11 +-
 29 files changed, 447 insertions(+), 144 deletions(-)

diff --git a/.agents/languages/java.md b/.agents/languages/java.md
index a96d04b7f..9367fd1dd 100644
--- a/.agents/languages/java.md
+++ b/.agents/languages/java.md
@@ -91,6 +91,11 @@ Load this file when changing anything under `java/` or when 
Java drives a cross-
   and locale types, `Float16`, `BFloat16`, and user-defined types remain 
registerable. Field/type
   `@JsonCodec`, `@JsonFormat`, and semantic metadata remain separate from 
exact registry mutation
   and are fixed by the target class or effective Mixin.
+- Fory JSON `byte[]` defaults to a Base64 string. `@JsonByteArray` selects 
required
+  `Format.BASE64` or `Format.ARRAY` for an exact byte-array field or getter in 
both directions.
+  Keep selection in the existing property codec path, including Mixin, Java 
processor, Kotlin
+  KSP, and GraalVM handling. Numeric arrays use signed-byte semantics and 
graph-memory accounting;
+  Base64 values remain binary leaves outside that budget.
 - Fory JSON `ObjectCodec` instances are resolver-owned and must not be 
registered directly. A
   language module that supplies a custom object model must use a 
`JsonCodecFactory`. A configurable
   factory's stable key must cover every option that can change its created 
codec class, object
diff --git a/docs/json/android.md b/docs/json/android.md
index 96db8a1c5..c9b706dfa 100644
--- a/docs/json/android.md
+++ b/docs/json/android.md
@@ -130,12 +130,12 @@ on the `ForyJson` builder that should use it:
 
 ```java
 import org.apache.fory.json.ForyJson;
-import org.apache.fory.json.annotation.JsonBase64;
+import org.apache.fory.json.annotation.JsonByteArray;
 import org.apache.fory.json.annotation.JsonMixin;
 
 @JsonMixin(target = ThirdPartyInvoice.class)
 public abstract class ThirdPartyInvoiceMixin {
-  @JsonBase64 byte[] signature;
+  @JsonByteArray(JsonByteArray.Format.BASE64) byte[] signature;
 }
 
 ForyJson json =
@@ -191,7 +191,7 @@ This reflection-based section applies to Java models. 
Kotlin models use the Kotl
 a minified Android build, apply KSP instead of writing broad package keep 
rules.
 
 Java `@JsonType` models support effective `JsonValidator`, `JsonValue`, 
`JsonRawValue`,
-`JsonBase64`, and `JsonFormat` annotations. Without `@JsonType`, those 
annotations still work
+`JsonByteArray`, and `JsonFormat` annotations. Without `@JsonType`, those 
annotations still work
 through reflection, but a release-minified application must keep the exact 
annotated members,
 annotation attributes, and codec constructor itself. A `JsonValue` method may 
use a non-JavaBean
 name, so its manual rule must name that method explicitly.
diff --git a/docs/json/annotations.md b/docs/json/annotations.md
index 46353322b..d06a0c72b 100644
--- a/docs/json/annotations.md
+++ b/docs/json/annotations.md
@@ -21,7 +21,7 @@ license: |
 
 Fory JSON provides these mapping and validation annotations in
 `org.apache.fory.json.annotation`:
-`JsonAnyGetter`, `JsonAnyProperty`, `JsonAnySetter`, `JsonBase64`, 
`JsonCodec`, `JsonCreator`, `JsonFormat`,
+`JsonAnyGetter`, `JsonAnyProperty`, `JsonAnySetter`, `JsonByteArray`, 
`JsonCodec`, `JsonCreator`, `JsonFormat`,
 `JsonIgnore`, `JsonProperty`, `JsonPropertyOrder`, `JsonRawValue`, 
`JsonSubTypes`, `JsonUnwrapped`,
 `JsonValidator`, and `JsonValue`. `JsonType` is a separate build-time model 
marker. They are
 Fory JSON APIs, not Jackson, Gson, or Fory binary-protocol compatibility 
annotations.
@@ -356,28 +356,36 @@ Any-property features are independent.
 as a trusted raw root value. That combination is serialization-only: the 
ordinary one-String
 `JsonCreator` cannot turn an input object or array into a String.
 
-## `JsonBase64`
+## `JsonByteArray`
 
-`JsonBase64` selects a quoted standard Base64 JSON string for one exact 
`byte[]` field or getter:
+Unannotated `byte[]` values use quoted standard Base64 JSON strings. 
`JsonByteArray` selects
+`BASE64` or `ARRAY` for one exact `byte[]` field or getter, in both reading 
and writing:
 
 ```java
-import org.apache.fory.json.annotation.JsonBase64;
+import org.apache.fory.json.annotation.JsonByteArray;
 
 public final class Attachment {
-  @JsonBase64
+  @JsonByteArray(JsonByteArray.Format.ARRAY)
+  public byte[] numbers;
+
+  @JsonByteArray(JsonByteArray.Format.BASE64)
   public byte[] content;
 }
 ```
 
-Bytes `{1, 2, 3}` are written as `{"content":"AQID"}` and decoded back to the 
original array.
-Fory writes the Base64 characters directly to the JSON output and decodes 
directly from the JSON
-input without creating an intermediate String. Standard Base64 padding is 
preserved. Java null
-follows the property's normal inclusion rule and reads from JSON null as null.
+For bytes `{1, -2, 3}`, `numbers` is written as `[1,-2,3]` and `content` as 
`"Af4D"`.
+`ARRAY` reads JSON arrays using the signed byte range `[-128, 127]`; `BASE64` 
reads standard
+Base64 strings and preserves padding when writing. Each representation also 
accepts JSON null,
+and null output follows the property's normal inclusion rule. The default 
Base64 codec does not
+accept numeric-array input; select `ARRAY` for a property that uses that 
format.
+
+The format is required when the annotation is present. It applies only to the 
annotated byte-array
+property, not to container elements or map values. Mixin declarations can 
select or remove it.
+It cannot share a logical property with `JsonRawValue`, an occurrence 
`JsonCodec`, `JsonFormat`,
+or an Any declaration. Conflicting formats on the field and getter of one 
property are rejected.
 
-The annotation is not a type-use annotation and does not change ordinary 
unannotated `byte[]`
-properties, container elements, or Map values. It cannot share a logical 
property with
-`JsonRawValue`, an occurrence `JsonCodec`, `JsonFormat`, or an Any 
declaration. The equivalent explicit codec is
-`@JsonCodec(Base64ByteArrayCodec.class)`.
+Base64 values are binary leaves excluded from the graph-memory budget. Numeric 
arrays count their
+array storage against that budget; see 
[Security](security.md#depth-and-graph-memory-limits).
 
 ## `JsonFormat`
 
@@ -439,7 +447,7 @@ unwrapped values are intentionally rejected. Types with 
ambiguous formatting sem
 legacy and SQL date types, `Duration`, `Period`, `TimeZone`, `ZoneId`, and 
`ZoneOffset`, are not
 supported. A wrapper with a complete registered, annotation-selected, 
polymorphic, or `JsonValue`
 representation is also rejected because that representation owns the whole 
wrapper.
-`JsonFormat` cannot share a field with `JsonCodec`, `JsonBase64`, 
`JsonRawValue`, `JsonAnyProperty`,
+`JsonFormat` cannot share a field with `JsonCodec`, `JsonByteArray`, 
`JsonRawValue`, `JsonAnyProperty`,
 `JsonUnwrapped`, or `JsonValue`.
 
 ## `JsonUnwrapped`
diff --git a/docs/json/graalvm.md b/docs/json/graalvm.md
index 08295063c..22d48b3f9 100644
--- a/docs/json/graalvm.md
+++ b/docs/json/graalvm.md
@@ -209,10 +209,10 @@ JVM and Android.
 
 `JsonValue` fields and effective public zero-argument methods are supported, 
including matching
 one-String `JsonCreator` constructors and public static factories. Fixed 
`JsonRawValue` fields and
-getters support trusted raw String values, and fixed `JsonBase64` fields and 
getters support Base64
-`byte[]` values as on the JVM. `JsonFormat` date/time fields use the same 
direct-field,
+getters support trusted raw String values, and `JsonByteArray` fields and 
getters select Base64 strings or numeric
+byte arrays as on the JVM. `JsonFormat` date/time fields use the same 
direct-field,
 one-wrapper-level, and `timezone` behavior as on the JVM. For direct target 
annotations, annotate
-each reachable owning model with `JsonType` so Native Image retains these 
members and the Base64
+each reachable owning model with `JsonType` so Native Image retains these 
members and the selected byte-array
 codec constructor.
 A directly annotated `JsonValue` Record uses its generated component accessor 
and canonical
 constructor operations. An effective declaration supplied by a Mixin uses the 
Mixin workflow above
diff --git a/docs/json/kotlin.md b/docs/json/kotlin.md
index 4b6bc1822..64ef261a4 100644
--- a/docs/json/kotlin.md
+++ b/docs/json/kotlin.md
@@ -280,7 +280,7 @@ their normal Fory JSON representation when used from Kotlin:
 | text                               | `String`, exact `CharSequence`, 
`StringBuilder`, and `StringBuffer` use String shapes                           
                                                                                
                                                                                
           |
 | arbitrary/reduced-precision number | `BigInteger`, `BigDecimal`, Fory 
`Float16`, and `BFloat16` use their core numeric shapes and limits              
                                                                                
                                                                                
          |
 | enum                               | quoted enum constant name               
                                                                                
                                                                                
                                                                                
   |
-| Java/Kotlin arrays                 | normal JSON arrays; `ByteArray` is 
numeric unless `JsonBase64` selects binary; unsigned semantic arrays are listed 
below                                                                           
                                                                                
        |
+| Java/Kotlin arrays                 | normal JSON arrays except `ByteArray`, 
which uses Base64 strings by default; 
`@field:JsonByteArray(JsonByteArray.Format.ARRAY)` selects numeric arrays; 
unsigned semantic arrays are listed below                                       
                                                   |
 | Optional and atomic                | `Optional<T>`, primitive Optionals, 
atomic scalars/references, and atomic arrays keep their transparent core shapes 
subject to the nullability rules above                                          
                                                                                
       |
 | quoted JDK values                  | `Currency`, `File`, `URI`, `Path`, 
`Pattern`, `UUID`, `Locale`, `Charset`, and `TimeZone` keep their core String 
shapes                                                                          
                                                                                
          |
 | legacy date/time                   | `Date`, `Calendar`, and available 
`java.sql.Date`, `Time`, and `Timestamp` keep their epoch-millisecond shapes    
                                                                                
                                                                                
         |
diff --git 
a/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/ForyJsonExample.java
 
b/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/ForyJsonExample.java
index ff5055edf..4d848b569 100644
--- 
a/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/ForyJsonExample.java
+++ 
b/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/ForyJsonExample.java
@@ -49,7 +49,7 @@ import org.apache.fory.json.annotation.ForyJsonProvider;
 import org.apache.fory.json.annotation.JsonAnyGetter;
 import org.apache.fory.json.annotation.JsonAnyProperty;
 import org.apache.fory.json.annotation.JsonAnySetter;
-import org.apache.fory.json.annotation.JsonBase64;
+import org.apache.fory.json.annotation.JsonByteArray;
 import org.apache.fory.json.annotation.JsonCodec;
 import org.apache.fory.json.annotation.JsonCreator;
 import org.apache.fory.json.annotation.JsonFormat;
@@ -489,6 +489,20 @@ public final class ForyJsonExample {
         new String(json.toJsonBytes(raw), 
StandardCharsets.UTF_8).equals("{\"body\":{\"id\":1}}"));
     Preconditions.checkArgument(
         json.fromJson("{\"body\":\"text\"}", 
RawValue.class).body.equals("text"));
+    ArrayBytes arrayBytes = new ArrayBytes();
+    arrayBytes.value = new byte[] {1, -2, 3};
+    
Preconditions.checkArgument(json.toJson(arrayBytes).equals("{\"value\":[1,-2,3]}"));
+    Preconditions.checkArgument(
+        new String(json.toJsonBytes(arrayBytes), StandardCharsets.UTF_8)
+            .equals("{\"value\":[1,-2,3]}"));
+    Preconditions.checkArgument(
+        Arrays.equals(
+            json.fromJson("{\"value\":[1,-2,3]}", ArrayBytes.class).value, 
arrayBytes.value));
+    Preconditions.checkArgument(
+        Arrays.equals(
+            
json.fromJson("{\"value\":[1,-2,3]}".getBytes(StandardCharsets.UTF_8), 
ArrayBytes.class)
+                .value,
+            arrayBytes.value));
     Base64Bytes base64Bytes = new Base64Bytes();
     base64Bytes.value = new byte[] {1, 2, 3};
     
Preconditions.checkArgument(json.toJson(base64Bytes).equals("{\"value\":\"AQID\"}"));
@@ -1273,9 +1287,16 @@ public final class ForyJsonExample {
     @JsonRawValue public String body;
   }
 
+  @JsonType
+  public static final class ArrayBytes {
+    @JsonByteArray(JsonByteArray.Format.ARRAY)
+    public byte[] value;
+  }
+
   @JsonType
   public static final class Base64Bytes {
-    @JsonBase64 public byte[] value;
+    @JsonByteArray(JsonByteArray.Format.BASE64)
+    public byte[] value;
   }
 
   @JsonType
diff --git 
a/integration_tests/kotlin_json_corpus/src/main/kotlin/org/apache/fory/integration/kotlin/json/corpus/PlatformCorpusChecks.kt
 
b/integration_tests/kotlin_json_corpus/src/main/kotlin/org/apache/fory/integration/kotlin/json/corpus/PlatformCorpusChecks.kt
index 7e8b4cf2e..754d82f3f 100644
--- 
a/integration_tests/kotlin_json_corpus/src/main/kotlin/org/apache/fory/integration/kotlin/json/corpus/PlatformCorpusChecks.kt
+++ 
b/integration_tests/kotlin_json_corpus/src/main/kotlin/org/apache/fory/integration/kotlin/json/corpus/PlatformCorpusChecks.kt
@@ -30,6 +30,9 @@ public object PlatformCorpusChecks {
     verifyRoot(decoded)
     val text = json.toJson(decoded, type)
     check(text.contains("\"display_label\":\"mixin\""))
+    check(text.contains("\"numbers\":[1,-2,3]"))
+    check(text.contains("\"binary\":\"Af4D\""))
+    check(text.contains("\"defaultBytes\":\"Af4D\""))
     verifyRoot(json.fromJson(text, type))
     verifyRoot(json.fromJson(json.toJsonBytes(decoded, type), type))
   }
@@ -43,5 +46,8 @@ public object PlatformCorpusChecks {
     check(actual.profile.label == expected.profile.label)
     check(actual.token == expected.token)
     check(actual.box == expected.box)
+    check(actual.numbers.contentEquals(expected.numbers))
+    check(actual.binary.contentEquals(expected.binary))
+    check(actual.defaultBytes.contentEquals(expected.defaultBytes))
   }
 }
diff --git 
a/integration_tests/kotlin_json_corpus/src/main/kotlin/org/apache/fory/integration/kotlin/json/corpus/PlatformModels.kt
 
b/integration_tests/kotlin_json_corpus/src/main/kotlin/org/apache/fory/integration/kotlin/json/corpus/PlatformModels.kt
index 865ad9ab0..41ed8b6cc 100644
--- 
a/integration_tests/kotlin_json_corpus/src/main/kotlin/org/apache/fory/integration/kotlin/json/corpus/PlatformModels.kt
+++ 
b/integration_tests/kotlin_json_corpus/src/main/kotlin/org/apache/fory/integration/kotlin/json/corpus/PlatformModels.kt
@@ -20,6 +20,7 @@
 package org.apache.fory.integration.kotlin.json.corpus
 
 import kotlin.jvm.JvmInline
+import org.apache.fory.json.annotation.JsonByteArray
 import org.apache.fory.json.annotation.JsonCodec
 import org.apache.fory.json.annotation.JsonMixin
 import org.apache.fory.json.annotation.JsonSubTypes
@@ -104,6 +105,11 @@ public data class PlatformRoot(
   public val profile: PlatformJavaProfile,
   @field:JsonCodec(PlatformTokenCodec::class) public val token: PlatformToken,
   public val box: PlatformBox<String>,
+  @field:JsonByteArray(JsonByteArray.Format.ARRAY)
+  public val numbers: ByteArray = byteArrayOf(1, -2, 3),
+  @get:JsonByteArray(JsonByteArray.Format.BASE64)
+  public val binary: ByteArray = byteArrayOf(1, -2, 3),
+  public val defaultBytes: ByteArray = byteArrayOf(1, -2, 3),
 )
 
 internal fun platformRootValue(): PlatformRoot =
diff --git 
a/integration_tests/kotlin_json_corpus/src/test/kotlin/org/apache/fory/integration/kotlin/json/corpus/KspRetentionResourceTest.kt
 
b/integration_tests/kotlin_json_corpus/src/test/kotlin/org/apache/fory/integration/kotlin/json/corpus/KspRetentionResourceTest.kt
index acc1f1f8a..fcd4a5d82 100644
--- 
a/integration_tests/kotlin_json_corpus/src/test/kotlin/org/apache/fory/integration/kotlin/json/corpus/KspRetentionResourceTest.kt
+++ 
b/integration_tests/kotlin_json_corpus/src/test/kotlin/org/apache/fory/integration/kotlin/json/corpus/KspRetentionResourceTest.kt
@@ -47,7 +47,14 @@ public class KspRetentionResourceTest {
     assertTrue(sealed.contains("class $PACKAGE.PlatformSquare"), sealed)
     assertTrue(sealed.contains("class $PACKAGE.PlatformOpen"), sealed)
     assertFalse(sealed.contains("class $PACKAGE.PlatformOpenDescendant"), 
sealed)
-    assertConstructor(rules("PlatformRoot"), "$PACKAGE.PlatformTokenCodec")
+    val root = rules("PlatformRoot")
+    assertConstructor(root, "$PACKAGE.PlatformTokenCodec")
+    assertTrue(
+      root.contains("@interface 
org.apache.fory.json.annotation.JsonByteArray"),
+      root,
+    )
+    assertConstructor(root, "org.apache.fory.json.codec.Base64ByteArrayCodec")
+    assertConstructor(root, 
"org.apache.fory.json.codec.ArrayCodec\$SignedByteArrayCodec")
     assertConstructor(
       rules("PlatformDirectOverride"),
       "$PACKAGE.PlatformDirectOverrideCodec",
diff --git 
a/java/fory-annotation-processor/src/main/java/org/apache/fory/annotation/processing/JsonMixinAnnotations.java
 
b/java/fory-annotation-processor/src/main/java/org/apache/fory/annotation/processing/JsonMixinAnnotations.java
index 0434aaec3..c22b2868c 100644
--- 
a/java/fory-annotation-processor/src/main/java/org/apache/fory/annotation/processing/JsonMixinAnnotations.java
+++ 
b/java/fory-annotation-processor/src/main/java/org/apache/fory/annotation/processing/JsonMixinAnnotations.java
@@ -53,7 +53,7 @@ final class JsonMixinAnnotations {
   private static final String JSON_ANY_GETTER = JSON_PACKAGE + 
".annotation.JsonAnyGetter";
   private static final String JSON_ANY_PROPERTY = JSON_PACKAGE + 
".annotation.JsonAnyProperty";
   private static final String JSON_ANY_SETTER = JSON_PACKAGE + 
".annotation.JsonAnySetter";
-  private static final String JSON_BASE64 = JSON_PACKAGE + 
".annotation.JsonBase64";
+  private static final String JSON_BYTE_ARRAY = JSON_PACKAGE + 
".annotation.JsonByteArray";
   private static final String JSON_CODEC = JSON_PACKAGE + 
".annotation.JsonCodec";
   private static final String JSON_CREATOR = JSON_PACKAGE + 
".annotation.JsonCreator";
   private static final String JSON_FORMAT = JSON_PACKAGE + 
".annotation.JsonFormat";
@@ -73,7 +73,7 @@ final class JsonMixinAnnotations {
                   JSON_ANY_GETTER,
                   JSON_ANY_PROPERTY,
                   JSON_ANY_SETTER,
-                  JSON_BASE64,
+                  JSON_BYTE_ARRAY,
                   JSON_CODEC,
                   JSON_CREATOR,
                   JSON_FORMAT,
diff --git 
a/java/fory-annotation-processor/src/main/java/org/apache/fory/annotation/processing/JsonTypeProcessor.java
 
b/java/fory-annotation-processor/src/main/java/org/apache/fory/annotation/processing/JsonTypeProcessor.java
index eab6c6e54..083cb22cd 100644
--- 
a/java/fory-annotation-processor/src/main/java/org/apache/fory/annotation/processing/JsonTypeProcessor.java
+++ 
b/java/fory-annotation-processor/src/main/java/org/apache/fory/annotation/processing/JsonTypeProcessor.java
@@ -71,7 +71,7 @@ final class JsonTypeProcessor {
   private static final String JSON_PROPERTY = JSON_PACKAGE + 
".annotation.JsonProperty";
   private static final String JSON_VALUE = JSON_PACKAGE + 
".annotation.JsonValue";
   private static final String JSON_RAW_VALUE = JSON_PACKAGE + 
".annotation.JsonRawValue";
-  private static final String JSON_BASE64 = JSON_PACKAGE + 
".annotation.JsonBase64";
+  private static final String JSON_BYTE_ARRAY = JSON_PACKAGE + 
".annotation.JsonByteArray";
   private static final String JSON_UNWRAPPED = JSON_PACKAGE + 
".annotation.JsonUnwrapped";
   private static final String JSON_VALIDATOR = JSON_PACKAGE + 
".annotation.JsonValidator";
   private static final String BASE64_CODEC = JSON_PACKAGE + 
".codec.Base64ByteArrayCodec";
@@ -569,8 +569,13 @@ final class JsonTypeProcessor {
   private void collectOccurrenceCodec(
       JsonMixinAnnotations annotations, Element element, Model model) {
     collectCodecAnnotation(annotationMirror(annotations, element, JSON_CODEC), 
model);
-    if (hasAnnotation(annotations, element, JSON_BASE64)) {
-      model.codecTypes.add(BASE64_CODEC);
+    AnnotationMirror byteArray = annotationMirror(annotations, element, 
JSON_BYTE_ARRAY);
+    if (byteArray != null) {
+      VariableElement format = (VariableElement) annotationValue(byteArray, 
"value").getValue();
+      model.codecTypes.add(
+          format.getSimpleName().contentEquals("ARRAY")
+              ? JSON_PACKAGE + ".codec.ArrayCodec$SignedByteArrayCodec"
+              : BASE64_CODEC);
     }
   }
 
@@ -1134,7 +1139,7 @@ final class JsonTypeProcessor {
         || hasAnnotation(annotations, method, JSON_CODEC)
         || hasAnnotation(annotations, method, JSON_VALUE)
         || hasAnnotation(annotations, method, JSON_RAW_VALUE)
-        || hasAnnotation(annotations, method, JSON_BASE64)
+        || hasAnnotation(annotations, method, JSON_BYTE_ARRAY)
         || hasAnnotation(annotations, method, JSON_VALIDATOR)
         || hasJsonAnnotations(annotations, method.getParameters())) {
       return true;
diff --git 
a/java/fory-annotation-processor/src/test/java/org/apache/fory/annotation/processing/JsonTypeProcessorTest.java
 
b/java/fory-annotation-processor/src/test/java/org/apache/fory/annotation/processing/JsonTypeProcessorTest.java
index 8aac74e26..6e7659b69 100644
--- 
a/java/fory-annotation-processor/src/test/java/org/apache/fory/annotation/processing/JsonTypeProcessorTest.java
+++ 
b/java/fory-annotation-processor/src/test/java/org/apache/fory/annotation/processing/JsonTypeProcessorTest.java
@@ -877,7 +877,7 @@ public class JsonTypeProcessorTest {
             "package test;\n"
                 + "import org.apache.fory.json.annotation.*;\n"
                 + "@JsonType public record EncodedRecord(\n"
-                + "    @JsonRawValue String raw, @JsonBase64 byte[] bytes) 
{}\n");
+                + "    @JsonRawValue String raw, 
@JsonByteArray(JsonByteArray.Format.ARRAY) byte[] bytes) {}\n");
     assertTrue(result.success, result.diagnostics());
     ClassLoader loader = result.classLoader();
     Class<?> type = loader.loadClass("test.EncodedRecord");
@@ -885,8 +885,8 @@ public class JsonTypeProcessorTest {
         type.getConstructor(String.class, byte[].class)
             .newInstance("{\"id\":1}", new byte[] {1, 2, 3});
     for (ForyJson json : jsonRuntimes(loader)) {
-      assertEquals(json.toJson(value), 
"{\"raw\":{\"id\":1},\"bytes\":\"AQID\"}");
-      Object decoded = json.fromJson("{\"raw\":\"text\",\"bytes\":\"AQI=\"}", 
type);
+      assertEquals(json.toJson(value), 
"{\"raw\":{\"id\":1},\"bytes\":[1,2,3]}");
+      Object decoded = json.fromJson("{\"raw\":\"text\",\"bytes\":[1,2]}", 
type);
       assertEquals(type.getMethod("raw").invoke(decoded), "text");
       assertTrue(
           Arrays.equals((byte[]) type.getMethod("bytes").invoke(decoded), new 
byte[] {1, 2}));
@@ -902,7 +902,7 @@ public class JsonTypeProcessorTest {
                 + "import java.util.Arrays;\n"
                 + "import org.apache.fory.json.annotation.*;\n"
                 + "@JsonType public final class EncodedCreator {\n"
-                + "  @JsonBase64 public final byte[] bytes;\n"
+                + "  @JsonByteArray(JsonByteArray.Format.BASE64) public final 
byte[] bytes;\n"
                 + "  @JsonCreator({\"bytes\"}) public EncodedCreator(byte[] 
bytes) {\n"
                 + "    this.bytes = bytes;\n"
                 + "  }\n"
@@ -1858,7 +1858,7 @@ public class JsonTypeProcessorTest {
   }
 
   @Test
-  public void valueRawAndBase64Rules() throws Exception {
+  public void valueRawAndByteArrayRules() throws Exception {
     Map<String, String> sources = new LinkedHashMap<>();
     sources.put(
         "test.ValueModel",
@@ -1875,7 +1875,8 @@ public class JsonTypeProcessorTest {
             + "import org.apache.fory.json.annotation.*;\n"
             + "@JsonType public final class RawModel {\n"
             + "  @JsonRawValue public String body;\n"
-            + "  @JsonBase64 public byte[] bytes;\n"
+            + "  @JsonByteArray(JsonByteArray.Format.BASE64) public byte[] 
bytes;\n"
+            + "  @JsonByteArray(JsonByteArray.Format.ARRAY) public byte[] 
numbers;\n"
             + "  private String other;\n"
             + "  @JsonRawValue public String getOther() { return other; }\n"
             + "  public void setOther(String other) { this.other = other; }\n"
@@ -1893,6 +1894,10 @@ public class JsonTypeProcessorTest {
         valueRules.contains("@interface 
org.apache.fory.json.annotation.JsonRawValue"), valueRules);
 
     String rawRules = result.generatedResource(RULE_PREFIX + 
"test.RawModel.pro");
+    assertTrue(
+        rawRules.contains(
+            "class org.apache.fory.json.codec.ArrayCodec$SignedByteArrayCodec 
{ public <init>(); }"),
+        rawRules);
     assertTrue(result.hasGeneratedSource("test/RawModel_ForyJsonCodec.java"));
     assertTrue(rawRules.contains("java.lang.String body;"), rawRules);
     assertTrue(rawRules.contains("byte[] bytes;"), rawRules);
@@ -1900,7 +1905,7 @@ public class JsonTypeProcessorTest {
     assertTrue(
         rawRules.contains("@interface 
org.apache.fory.json.annotation.JsonRawValue"), rawRules);
     assertTrue(
-        rawRules.contains("@interface 
org.apache.fory.json.annotation.JsonBase64"), rawRules);
+        rawRules.contains("@interface 
org.apache.fory.json.annotation.JsonByteArray"), rawRules);
     assertFalse(
         rawRules.contains("@interface 
org.apache.fory.json.annotation.JsonCodec"), rawRules);
     assertTrue(
diff --git 
a/java/fory-json/src/main/java/org/apache/fory/json/annotation/JsonBase64.java 
b/java/fory-json/src/main/java/org/apache/fory/json/annotation/JsonByteArray.java
similarity index 59%
rename from 
java/fory-json/src/main/java/org/apache/fory/json/annotation/JsonBase64.java
rename to 
java/fory-json/src/main/java/org/apache/fory/json/annotation/JsonByteArray.java
index 1046bd346..b221093bd 100644
--- 
a/java/fory-json/src/main/java/org/apache/fory/json/annotation/JsonBase64.java
+++ 
b/java/fory-json/src/main/java/org/apache/fory/json/annotation/JsonByteArray.java
@@ -26,14 +26,25 @@ import java.lang.annotation.RetentionPolicy;
 import java.lang.annotation.Target;
 
 /**
- * Selects a quoted standard Base64 JSON string as the representation of one 
exact {@code byte[]}
- * field or getter.
+ * Selects the JSON representation of one exact {@code byte[]} field or getter 
for both reading and
+ * writing. Unannotated byte arrays use a quoted standard Base64 string.
  *
- * <p>Writing encodes the bytes without an intermediate String, and reading 
decodes the JSON string
- * directly into bytes. Null inclusion and omission follow the property's 
normal configuration, and
- * an included null is written as JSON {@code null}.
+ * <p>Null inclusion and omission follow the property's normal configuration, 
and an included null
+ * is written as JSON {@code null}. This annotation cannot be combined with 
{@link JsonCodec} on the
+ * same logical property.
  */
 @Documented
 @Retention(RetentionPolicy.RUNTIME)
 @Target({ElementType.FIELD, ElementType.METHOD})
-public @interface JsonBase64 {}
+public @interface JsonByteArray {
+  /** Returns the representation used when reading and writing this property. 
*/
+  Format value();
+
+  /** The supported JSON representations of a byte array. */
+  enum Format {
+    /** A quoted standard Base64 string with padding. */
+    BASE64,
+    /** A JSON array of signed byte values in the range {@code [-128, 127]}. */
+    ARRAY
+  }
+}
diff --git 
a/java/fory-json/src/main/java/org/apache/fory/json/annotation/JsonMixin.java 
b/java/fory-json/src/main/java/org/apache/fory/json/annotation/JsonMixin.java
index fb73e516b..d62e61c99 100644
--- 
a/java/fory-json/src/main/java/org/apache/fory/json/annotation/JsonMixin.java
+++ 
b/java/fory-json/src/main/java/org/apache/fory/json/annotation/JsonMixin.java
@@ -37,7 +37,7 @@ import java.lang.annotation.Target;
  * access, invocation, and value.
  *
  * <p>A Mixin may contribute {@link JsonAnyGetter}, {@link JsonAnyProperty}, 
{@link JsonAnySetter},
- * {@link JsonBase64}, {@link JsonCodec}, {@link JsonCreator}, {@link 
JsonFormat}, {@link
+ * {@link JsonByteArray}, {@link JsonCodec}, {@link JsonCreator}, {@link 
JsonFormat}, {@link
  * JsonIgnore}, {@link JsonProperty}, {@link JsonPropertyOrder}, {@link 
JsonRawValue}, {@link
  * JsonSubTypes}, {@link JsonUnwrapped}, {@link JsonValidator}, and {@link 
JsonValue}. {@link
  * JsonType} remains a marker declared directly on a model and cannot be 
contributed or removed by a
diff --git 
a/java/fory-json/src/main/java/org/apache/fory/json/codec/ArrayCodec.java 
b/java/fory-json/src/main/java/org/apache/fory/json/codec/ArrayCodec.java
index 86c2a0244..6372628f0 100644
--- a/java/fory-json/src/main/java/org/apache/fory/json/codec/ArrayCodec.java
+++ b/java/fory-json/src/main/java/org/apache/fory/json/codec/ArrayCodec.java
@@ -58,7 +58,7 @@ public abstract class ArrayCodec<T> implements 
JsonValueCodec<T> {
     this.componentType = componentType;
   }
 
-  public static <T> ArrayCodec<T> create(
+  public static <T> JsonValueCodec<T> create(
       Class<T> arrayType, TypeRef<?> arrayTypeRef, JsonTypeResolver resolver) {
     if (!arrayType.isArray()) {
       throw new ForyJsonException("Unsupported JSON array type " + arrayType);
@@ -70,7 +70,7 @@ public abstract class ArrayCodec<T> implements 
JsonValueCodec<T> {
   }
 
   @Internal
-  public static <T> ArrayCodec<T> create(Class<T> arrayType, JsonTypeInfo 
componentTypeInfo) {
+  public static <T> JsonValueCodec<T> create(Class<T> arrayType, JsonTypeInfo 
componentTypeInfo) {
     if (!arrayType.isArray()) {
       throw new ForyJsonException("Unsupported JSON array type " + arrayType);
     }
@@ -90,7 +90,7 @@ public abstract class ArrayCodec<T> implements 
JsonValueCodec<T> {
         && componentCodec == ScalarCodecs.ShortCodec.PRIMITIVE) {
       return bind(ShortArrayCodec.INSTANCE);
     } else if (componentType == byte.class && componentCodec == 
ScalarCodecs.ByteCodec.PRIMITIVE) {
-      return bind(ByteArrayCodec.INSTANCE);
+      return bind(Base64ByteArrayCodec.INSTANCE);
     } else if (componentType == char.class && componentCodec == 
ScalarCodecs.CharCodec.PRIMITIVE) {
       return bind(CharArrayCodec.INSTANCE);
     } else if (componentType == float.class
@@ -161,7 +161,7 @@ public abstract class ArrayCodec<T> implements 
JsonValueCodec<T> {
 
   /** Returns the exact unsigned primitive-array specialization for one 
semantic array id. */
   @Internal
-  public static <T> ArrayCodec<T> createUnsignedPrimitive(
+  public static <T> JsonValueCodec<T> createUnsignedPrimitive(
       Class<T> arrayType, int typeId, boolean writeLongAsString) {
     if (arrayType == byte[].class && typeId == Types.UINT8_ARRAY) {
       return bind(ByteArrayCodec.UNSIGNED);
@@ -183,9 +183,9 @@ public abstract class ArrayCodec<T> implements 
JsonValueCodec<T> {
   }
 
   @SuppressWarnings("unchecked")
-  private static <T> ArrayCodec<T> bind(ArrayCodec<?> codec) {
+  private static <T> JsonValueCodec<T> bind(JsonValueCodec<?> codec) {
     // The factory has matched the runtime array class to this exact singleton 
implementation.
-    return (ArrayCodec<T>) codec;
+    return (JsonValueCodec<T>) codec;
   }
 
   // Package visibility lets Java 8 nested codecs call these helpers without 
synthetic accessors.
@@ -1262,7 +1262,6 @@ public abstract class ArrayCodec<T> implements 
JsonValueCodec<T> {
   }
 
   public abstract static class ByteArrayCodec extends ArrayCodec<byte[]> {
-    private static final ByteArrayCodec INSTANCE = new SignedByteArrayCodec();
     private static final ByteArrayCodec UNSIGNED = new 
UnsignedByteArrayCodec();
 
     private ByteArrayCodec() {
@@ -1394,7 +1393,10 @@ public abstract class ArrayCodec<T> implements 
JsonValueCodec<T> {
     }
   }
 
-  private static final class SignedByteArrayCodec extends ByteArrayCodec {
+  /** A complete {@code byte[]} codec using a JSON array of signed byte 
values. */
+  public static final class SignedByteArrayCodec extends ByteArrayCodec {
+    public SignedByteArrayCodec() {}
+
     @Override
     void writeElement(StringJsonWriter writer, byte value) {
       writer.writeInt(value);
diff --git 
a/java/fory-json/src/main/java/org/apache/fory/json/codec/Base64ByteArrayCodec.java
 
b/java/fory-json/src/main/java/org/apache/fory/json/codec/Base64ByteArrayCodec.java
index 9fa6786af..32f1f77ef 100644
--- 
a/java/fory-json/src/main/java/org/apache/fory/json/codec/Base64ByteArrayCodec.java
+++ 
b/java/fory-json/src/main/java/org/apache/fory/json/codec/Base64ByteArrayCodec.java
@@ -27,6 +27,8 @@ import org.apache.fory.json.writer.Utf8JsonWriter;
 
 /** A complete {@code byte[]} codec using a quoted standard Base64 JSON 
string. */
 public final class Base64ByteArrayCodec implements JsonValueCodec<byte[]> {
+  public static final Base64ByteArrayCodec INSTANCE = new 
Base64ByteArrayCodec();
+
   public Base64ByteArrayCodec() {}
 
   @Override
diff --git 
a/java/fory-json/src/main/java/org/apache/fory/json/codec/ObjectCodecBuilder.java
 
b/java/fory-json/src/main/java/org/apache/fory/json/codec/ObjectCodecBuilder.java
index 0a8120e6d..eb8b70d1d 100644
--- 
a/java/fory-json/src/main/java/org/apache/fory/json/codec/ObjectCodecBuilder.java
+++ 
b/java/fory-json/src/main/java/org/apache/fory/json/codec/ObjectCodecBuilder.java
@@ -42,7 +42,7 @@ import org.apache.fory.json.PropertyNamingStrategy;
 import org.apache.fory.json.annotation.JsonAnyGetter;
 import org.apache.fory.json.annotation.JsonAnyProperty;
 import org.apache.fory.json.annotation.JsonAnySetter;
-import org.apache.fory.json.annotation.JsonBase64;
+import org.apache.fory.json.annotation.JsonByteArray;
 import org.apache.fory.json.annotation.JsonCodec;
 import org.apache.fory.json.annotation.JsonCreator;
 import org.apache.fory.json.annotation.JsonFormat;
@@ -1079,7 +1079,7 @@ final class ObjectCodecBuilder {
         || method.isAnnotationPresent(JsonAnySetter.class)
         || method.isAnnotationPresent(JsonValue.class)
         || method.isAnnotationPresent(JsonRawValue.class)
-        || method.isAnnotationPresent(JsonBase64.class)
+        || method.isAnnotationPresent(JsonByteArray.class)
         || method.isAnnotationPresent(JsonValidator.class)) {
       return true;
     }
@@ -1094,7 +1094,7 @@ final class ObjectCodecBuilder {
     return method.isAnnotationPresent(JsonAnyGetter.class)
         || method.isAnnotationPresent(JsonValue.class)
         || method.isAnnotationPresent(JsonRawValue.class)
-        || method.isAnnotationPresent(JsonBase64.class)
+        || method.isAnnotationPresent(JsonByteArray.class)
         || getterPropertyName(method) != null;
   }
 
@@ -1882,8 +1882,8 @@ final class ObjectCodecBuilder {
         if (annotations.has(field, JsonFormat.class)) {
           validateFormatField(field, annotations);
         }
-        if (annotations.has(field, JsonBase64.class)) {
-          validateBase64Field(field, annotations);
+        if (annotations.has(field, JsonByteArray.class)) {
+          validateByteArrayField(field, annotations);
         }
         if (annotations.has(field, JsonRawValue.class)) {
           validateRawField(field, annotations);
@@ -1940,8 +1940,8 @@ final class ObjectCodecBuilder {
           validateRawMethod(
               type, method, propertyDiscoveryEnabled, record, generatedCodec, 
annotations);
         }
-        if (annotations.has(method, JsonBase64.class)) {
-          validateBase64Method(
+        if (annotations.has(method, JsonByteArray.class)) {
+          validateByteArrayMethod(
               type, method, propertyDiscoveryEnabled, record, generatedCodec, 
annotations);
         }
         if (annotations.has(method, JsonUnwrapped.class)) {
@@ -2008,8 +2008,8 @@ final class ObjectCodecBuilder {
         validateRawMethod(
             type, method, propertyDiscoveryEnabled, record, generatedCodec, 
annotations);
       }
-      if (annotations.has(method, JsonBase64.class)) {
-        validateBase64Method(
+      if (annotations.has(method, JsonByteArray.class)) {
+        validateByteArrayMethod(
             type, method, propertyDiscoveryEnabled, record, generatedCodec, 
annotations);
       }
       if (annotations.has(method, JsonUnwrapped.class)) {
@@ -2307,7 +2307,7 @@ final class ObjectCodecBuilder {
       throw new ForyJsonException("Invalid @JsonRawValue field " + field);
     }
     if (annotations.has(field, JsonCodec.class)
-        || annotations.has(field, JsonBase64.class)
+        || annotations.has(field, JsonByteArray.class)
         || annotations.has(field, JsonAnyProperty.class)) {
       throw new ForyJsonException("Conflicting JSON annotations on 
@JsonRawValue field " + field);
     }
@@ -2339,24 +2339,24 @@ final class ObjectCodecBuilder {
       throw new ForyJsonException("Invalid @JsonRawValue method " + method);
     }
     if (annotations.has(method, JsonCodec.class)
-        || annotations.has(method, JsonBase64.class)
+        || annotations.has(method, JsonByteArray.class)
         || annotations.has(method, JsonAnyGetter.class)) {
       throw new ForyJsonException("Conflicting JSON annotations on 
@JsonRawValue method " + method);
     }
   }
 
-  private static void validateBase64Field(Field field, Annotations 
annotations) {
+  private static void validateByteArrayField(Field field, Annotations 
annotations) {
     if (!isEligibleField(field) || field.getType() != byte[].class) {
-      throw new ForyJsonException("Invalid @JsonBase64 field " + field);
+      throw new ForyJsonException("Invalid @JsonByteArray field " + field);
     }
     if (annotations.has(field, JsonCodec.class)
         || annotations.has(field, JsonRawValue.class)
         || annotations.has(field, JsonAnyProperty.class)) {
-      throw new ForyJsonException("Conflicting JSON annotations on @JsonBase64 
field " + field);
+      throw new ForyJsonException("Conflicting JSON annotations on 
@JsonByteArray field " + field);
     }
     JsonIgnore ignore = annotations.get(field, JsonIgnore.class);
     if (ignore != null && ignore.ignoreRead() && ignore.ignoreWrite()) {
-      throw new ForyJsonException("@JsonBase64 has no JSON read or write 
direction: " + field);
+      throw new ForyJsonException("@JsonByteArray has no JSON read or write 
direction: " + field);
     }
   }
 
@@ -2365,7 +2365,7 @@ final class ObjectCodecBuilder {
       throw new ForyJsonException("Invalid @JsonFormat field " + field);
     }
     if (annotations.has(field, JsonCodec.class)
-        || annotations.has(field, JsonBase64.class)
+        || annotations.has(field, JsonByteArray.class)
         || annotations.has(field, JsonRawValue.class)
         || annotations.has(field, JsonAnyProperty.class)
         || annotations.has(field, JsonUnwrapped.class)
@@ -2378,7 +2378,7 @@ final class ObjectCodecBuilder {
     }
   }
 
-  private static void validateBase64Method(
+  private static void validateByteArrayMethod(
       Class<?> type,
       Method method,
       boolean propertyDiscoveryEnabled,
@@ -2388,7 +2388,7 @@ final class ObjectCodecBuilder {
     if ((!propertyDiscoveryEnabled
             && !(record
                 && isPropagatedRecordAnnotation(
-                    type, method, JsonBase64.class, generatedCodec, 
annotations)))
+                    type, method, JsonByteArray.class, generatedCodec, 
annotations)))
         || !isEligibleAccessor(method)
         || method.isVarArgs()
         || method.getTypeParameters().length != 0
@@ -2396,12 +2396,13 @@ final class ObjectCodecBuilder {
         || method.getReturnType() != byte[].class
         || ((!record && getterPropertyName(method) == null)
             || (record && !isRecordAccessor(type, method, generatedCodec)))) {
-      throw new ForyJsonException("Invalid @JsonBase64 method " + method);
+      throw new ForyJsonException("Invalid @JsonByteArray method " + method);
     }
     if (annotations.has(method, JsonCodec.class)
         || annotations.has(method, JsonRawValue.class)
         || annotations.has(method, JsonAnyGetter.class)) {
-      throw new ForyJsonException("Conflicting JSON annotations on @JsonBase64 
method " + method);
+      throw new ForyJsonException(
+          "Conflicting JSON annotations on @JsonByteArray method " + method);
     }
   }
 
@@ -3393,18 +3394,25 @@ final class ObjectCodecBuilder {
 
     private void mergeCodec(AnnotatedElement source) {
       JsonCodec declared = annotations.get(source, JsonCodec.class);
-      if (annotations.has(source, JsonBase64.class)) {
+      JsonByteArray byteArray = annotations.get(source, JsonByteArray.class);
+      if (byteArray != null) {
         if (formatAnnotation != null) {
-          throw formatConflict(source, "@JsonBase64");
+          throw formatConflict(source, "@JsonByteArray");
         }
         if (declared != null || codecAnnotation != null) {
           throw new ForyJsonException(
-              "@JsonBase64 cannot coexist with @JsonCodec for property " + 
name);
+              "@JsonByteArray cannot coexist with @JsonCodec for property " + 
name);
         }
-        if (valueCodecClass == null) {
-          valueCodecClass = Base64ByteArrayCodec.class;
-          codecSource = source;
+        Class<? extends JsonValueCodec<?>> codecClass =
+            byteArray.value() == JsonByteArray.Format.ARRAY
+                ? ArrayCodec.SignedByteArrayCodec.class
+                : Base64ByteArrayCodec.class;
+        if (valueCodecClass != null && valueCodecClass != codecClass) {
+          throw new ForyJsonException(
+              "Conflicting @JsonByteArray declarations for property " + name);
         }
+        valueCodecClass = codecClass;
+        codecSource = source;
         return;
       }
       if (declared != null && formatAnnotation != null) {
@@ -3412,7 +3420,7 @@ final class ObjectCodecBuilder {
       }
       if (declared != null && valueCodecClass != null) {
         throw new ForyJsonException(
-            "@JsonBase64 cannot coexist with @JsonCodec for property " + name);
+            "@JsonByteArray cannot coexist with @JsonCodec for property " + 
name);
       }
       if (declared == null) {
         return;
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 1438f0143..aa6678818 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
@@ -502,6 +502,45 @@ public abstract class JsonReader {
       throw error("Expected Base64 JSON string");
     }
     int bodyStart = position;
+    int end = -1;
+    int padding = 0;
+    while (position < length()) {
+      char ch = charAt(position++);
+      if (ch == '"') {
+        end = position - 1;
+        break;
+      }
+      if (ch == '\\') {
+        // Rare: an escaped character in the body, fall back to the validating 
two-pass path
+        position = bodyStart;
+        return readBase64Escaped(bodyStart);
+      }
+      if (ch == '=') {
+        if (++padding > 2) {
+          throw error("Invalid Base64 JSON string padding");
+        }
+      } else if (padding != 0 || base64Digit(ch) < 0) {
+        throw error("Invalid Base64 JSON string");
+      }
+    }
+    if (end < 0) {
+      throw error("Unterminated Base64 JSON string");
+    }
+    int bodyLength = end - bodyStart;
+    if (bodyLength == 0) {
+      return EMPTY_BYTES;
+    }
+    if ((bodyLength & 3) != 0) {
+      throw error("Invalid Base64 JSON string length");
+    }
+    int decodedLength = (bodyLength >>> 2) * 3 - padding;
+    // Base64 is a binary leaf: validated input bounds its storage, not the 
graph memory budget.
+    byte[] decoded = new byte[decodedLength];
+    decodeBase64(decoded, bodyStart, end);
+    return decoded;
+  }
+
+  private byte[] readBase64Escaped(int bodyStart) {
     // Validate and consume the complete encoded text before allocating, so 
untrusted input can
     // only request decoded storage proportional to code units already proven 
readable.
     long shape = scanBase64Shape();
@@ -511,9 +550,10 @@ public abstract class JsonReader {
     }
     int end = position;
     int padding = (int) (shape & 3);
-    byte[] decoded = new byte[(encodedLength >>> 2) * 3 - padding];
+    int decodedLength = (encodedLength >>> 2) * 3 - padding;
+    byte[] decoded = new byte[decodedLength];
     position = bodyStart;
-    decodeBase64(decoded, encodedLength);
+    decodeBase64Escaped(decoded, encodedLength);
     position = end;
     return decoded;
   }
@@ -539,7 +579,7 @@ public abstract class JsonReader {
           throw error("Invalid Base64 JSON string padding");
         }
       } else {
-        if (padding != 0 || decodeBase64Digit(ch) < 0) {
+        if (padding != 0 || base64Digit(ch) < 0) {
           throw error("Invalid Base64 JSON string");
         }
       }
@@ -548,18 +588,39 @@ public abstract class JsonReader {
     throw error("Unterminated Base64 JSON string");
   }
 
-  private void decodeBase64(byte[] decoded, int encodedLength) {
+  private void decodeBase64(byte[] decoded, int start, int end) {
+    int output = 0;
+    for (int index = start; index < end; index += 4) {
+      int bits = (base64Digit(charAt(index)) << 18) | 
(base64Digit(charAt(index + 1)) << 12);
+      char third = charAt(index + 2);
+      char fourth = charAt(index + 3);
+      if (third != '=') {
+        bits |= base64Digit(third) << 6;
+      }
+      if (fourth != '=') {
+        bits |= base64Digit(fourth);
+      }
+      decoded[output++] = (byte) (bits >>> 16);
+      if (output < decoded.length) {
+        decoded[output++] = (byte) (bits >>> 8);
+        if (output < decoded.length) {
+          decoded[output++] = (byte) bits;
+        }
+      }
+    }
+  }
+
+  private void decodeBase64Escaped(byte[] decoded, int encodedLength) {
     int output = 0;
     for (int index = 0; index < encodedLength; index += 4) {
-      int bits =
-          (decodeBase64Digit(readBase64Char()) << 18) | 
(decodeBase64Digit(readBase64Char()) << 12);
+      int bits = (base64Digit(readBase64Char()) << 18) | 
(base64Digit(readBase64Char()) << 12);
       char third = readBase64Char();
       char fourth = readBase64Char();
       if (third != '=') {
-        bits |= decodeBase64Digit(third) << 6;
+        bits |= base64Digit(third) << 6;
       }
       if (fourth != '=') {
-        bits |= decodeBase64Digit(fourth);
+        bits |= base64Digit(fourth);
       }
       decoded[output++] = (byte) (bits >>> 16);
       if (output < decoded.length) {
@@ -576,20 +637,25 @@ public abstract class JsonReader {
     return ch == '\\' ? readEscapedFieldNameChar() : ch;
   }
 
-  private static int decodeBase64Digit(char ch) {
-    if (ch >= 'A' && ch <= 'Z') {
-      return ch - 'A';
-    }
-    if (ch >= 'a' && ch <= 'z') {
-      return ch - 'a' + 26;
+  private static final byte[] BASE64_DIGIT_VALUES = new byte[128];
+
+  static {
+    java.util.Arrays.fill(BASE64_DIGIT_VALUES, (byte) -1);
+    for (char c = 'A'; c <= 'Z'; c++) {
+      BASE64_DIGIT_VALUES[c] = (byte) (c - 'A');
     }
-    if (ch >= '0' && ch <= '9') {
-      return ch - '0' + 52;
+    for (char c = 'a'; c <= 'z'; c++) {
+      BASE64_DIGIT_VALUES[c] = (byte) (c - 'a' + 26);
     }
-    if (ch == '+') {
-      return 62;
+    for (char c = '0'; c <= '9'; c++) {
+      BASE64_DIGIT_VALUES[c] = (byte) (c - '0' + 52);
     }
-    return ch == '/' ? 63 : -1;
+    BASE64_DIGIT_VALUES['+'] = 62;
+    BASE64_DIGIT_VALUES['/'] = 63;
+  }
+
+  private static int base64Digit(char ch) {
+    return ch < 128 ? BASE64_DIGIT_VALUES[ch] : -1;
   }
 
   public String readCharSequence() {
diff --git 
a/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonMixinAnnotations.java
 
b/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonMixinAnnotations.java
index 2c1e99207..5752525b9 100644
--- 
a/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonMixinAnnotations.java
+++ 
b/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonMixinAnnotations.java
@@ -45,7 +45,7 @@ import org.apache.fory.json.JsonConfig;
 import org.apache.fory.json.annotation.JsonAnyGetter;
 import org.apache.fory.json.annotation.JsonAnyProperty;
 import org.apache.fory.json.annotation.JsonAnySetter;
-import org.apache.fory.json.annotation.JsonBase64;
+import org.apache.fory.json.annotation.JsonByteArray;
 import org.apache.fory.json.annotation.JsonCodec;
 import org.apache.fory.json.annotation.JsonCreator;
 import org.apache.fory.json.annotation.JsonFormat;
@@ -69,7 +69,7 @@ final class JsonMixinAnnotations {
         JsonAnyGetter.class,
         JsonAnyProperty.class,
         JsonAnySetter.class,
-        JsonBase64.class,
+        JsonByteArray.class,
         JsonCodec.class,
         JsonCreator.class,
         JsonFormat.class,
diff --git 
a/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonValueDeclaration.java
 
b/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonValueDeclaration.java
index 6193ac5b8..992a8e56e 100644
--- 
a/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonValueDeclaration.java
+++ 
b/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonValueDeclaration.java
@@ -31,7 +31,7 @@ import java.util.List;
 import org.apache.fory.json.ForyJsonException;
 import org.apache.fory.json.annotation.JsonAnyGetter;
 import org.apache.fory.json.annotation.JsonAnyProperty;
-import org.apache.fory.json.annotation.JsonBase64;
+import org.apache.fory.json.annotation.JsonByteArray;
 import org.apache.fory.json.annotation.JsonCodec;
 import org.apache.fory.json.annotation.JsonFormat;
 import org.apache.fory.json.annotation.JsonIgnore;
@@ -187,7 +187,7 @@ final class JsonValueDeclaration {
       throw new ForyJsonException("Invalid @JsonValue field " + field);
     }
     if (registry.annotation(type, field, JsonCodec.class) != null
-        || registry.annotation(type, field, JsonBase64.class) != null
+        || registry.annotation(type, field, JsonByteArray.class) != null
         || registry.annotation(type, field, JsonFormat.class) != null
         || registry.annotation(type, field, JsonAnyProperty.class) != null
         || registry.annotation(type, field, JsonUnwrapped.class) != null
@@ -209,7 +209,7 @@ final class JsonValueDeclaration {
       throw new ForyJsonException("Invalid @JsonValue method " + method);
     }
     if (registry.annotation(type, method, JsonCodec.class) != null
-        || registry.annotation(type, method, JsonBase64.class) != null
+        || registry.annotation(type, method, JsonByteArray.class) != null
         || registry.annotation(type, method, JsonAnyGetter.class) != null
         || registry.annotation(type, method, JsonUnwrapped.class) != null
         || registry.annotation(type, method, JsonIgnore.class) != null) {
diff --git 
a/java/fory-json/src/main/java17/org/apache/fory/json/ForyJsonGraalVMFeature.java
 
b/java/fory-json/src/main/java17/org/apache/fory/json/ForyJsonGraalVMFeature.java
index d5de194e4..d5f986899 100644
--- 
a/java/fory-json/src/main/java17/org/apache/fory/json/ForyJsonGraalVMFeature.java
+++ 
b/java/fory-json/src/main/java17/org/apache/fory/json/ForyJsonGraalVMFeature.java
@@ -51,7 +51,7 @@ import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
 import org.apache.fory.json.annotation.ForyJsonProvider;
 import org.apache.fory.json.annotation.JsonAnySetter;
-import org.apache.fory.json.annotation.JsonBase64;
+import org.apache.fory.json.annotation.JsonByteArray;
 import org.apache.fory.json.annotation.JsonCodec;
 import org.apache.fory.json.annotation.JsonCreator;
 import org.apache.fory.json.annotation.JsonMixin;
@@ -60,6 +60,7 @@ import org.apache.fory.json.annotation.JsonType;
 import org.apache.fory.json.annotation.JsonUnwrapped;
 import org.apache.fory.json.annotation.JsonValidator;
 import org.apache.fory.json.annotation.JsonValue;
+import org.apache.fory.json.codec.ArrayCodec;
 import org.apache.fory.json.codec.Base64ByteArrayCodec;
 import org.apache.fory.json.codec.JsonUnwrappedInfo;
 import org.apache.fory.json.codec.ObjectCodec;
@@ -999,8 +1000,12 @@ final class ForyJsonGraalVMFeature implements Feature {
 
   private void registerOccurrenceCodecs(JsonMixinView annotations, 
AnnotatedElement element) {
     registerCodecs(annotation(annotations, element, JsonCodec.class));
-    if (annotation(annotations, element, JsonBase64.class) != null) {
-      registerCodec(Base64ByteArrayCodec.class);
+    JsonByteArray byteArray = annotation(annotations, element, 
JsonByteArray.class);
+    if (byteArray != null) {
+      registerCodec(
+          byteArray.value() == JsonByteArray.Format.ARRAY
+              ? ArrayCodec.SignedByteArrayCodec.class
+              : Base64ByteArrayCodec.class);
     }
   }
 
diff --git 
a/java/fory-json/src/test/java/org/apache/fory/json/JsonAndroidRuntimeTest.java 
b/java/fory-json/src/test/java/org/apache/fory/json/JsonAndroidRuntimeTest.java
index dc78da672..b2e87a27b 100644
--- 
a/java/fory-json/src/test/java/org/apache/fory/json/JsonAndroidRuntimeTest.java
+++ 
b/java/fory-json/src/test/java/org/apache/fory/json/JsonAndroidRuntimeTest.java
@@ -34,7 +34,7 @@ import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
-import org.apache.fory.json.annotation.JsonBase64;
+import org.apache.fory.json.annotation.JsonByteArray;
 import org.apache.fory.json.annotation.JsonCodec;
 import org.apache.fory.json.annotation.JsonCreator;
 import org.apache.fory.json.annotation.JsonFormat;
@@ -257,7 +257,9 @@ public class JsonAndroidRuntimeTest {
 
   public static final class AndroidRaw {
     @JsonRawValue public String body;
-    @JsonBase64 public byte[] bytes;
+
+    @JsonByteArray(JsonByteArray.Format.BASE64)
+    public byte[] bytes;
   }
 
   public static final class AndroidFormat {
diff --git 
a/java/fory-json/src/test/java/org/apache/fory/json/JsonBase64AnnotationTest.java
 
b/java/fory-json/src/test/java/org/apache/fory/json/JsonByteArrayAnnotationTest.java
similarity index 72%
rename from 
java/fory-json/src/test/java/org/apache/fory/json/JsonBase64AnnotationTest.java
rename to 
java/fory-json/src/test/java/org/apache/fory/json/JsonByteArrayAnnotationTest.java
index 025c2a66c..08c115072 100644
--- 
a/java/fory-json/src/test/java/org/apache/fory/json/JsonBase64AnnotationTest.java
+++ 
b/java/fory-json/src/test/java/org/apache/fory/json/JsonByteArrayAnnotationTest.java
@@ -27,7 +27,7 @@ import java.nio.charset.StandardCharsets;
 import java.util.Map;
 import org.apache.fory.json.annotation.JsonAnyGetter;
 import org.apache.fory.json.annotation.JsonAnyProperty;
-import org.apache.fory.json.annotation.JsonBase64;
+import org.apache.fory.json.annotation.JsonByteArray;
 import org.apache.fory.json.annotation.JsonCodec;
 import org.apache.fory.json.annotation.JsonCreator;
 import org.apache.fory.json.annotation.JsonIgnore;
@@ -40,12 +40,81 @@ import org.testng.SkipException;
 import org.testng.annotations.Factory;
 import org.testng.annotations.Test;
 
-public class JsonBase64AnnotationTest extends ForyJsonTestModels {
+public class JsonByteArrayAnnotationTest extends ForyJsonTestModels {
   @Factory(dataProvider = "enableCodegen")
-  public JsonBase64AnnotationTest(boolean codegen) {
+  public JsonByteArrayAnnotationTest(boolean codegen) {
     super(codegen);
   }
 
+  @Test
+  public void arrayRoundTrip() {
+    ForyJson json = newJson();
+    ArrayField value = new ArrayField();
+    byte[][] values = {new byte[0], {-128}, {1, -2, 127}};
+    String[] encoded = {"[]", "[-128]", "[1,-2,127]"};
+    for (int i = 0; i < values.length; i++) {
+      value.bytes = values[i];
+      String text = "{\"bytes\":" + encoded[i] + "}";
+      assertEquals(json.toJson(value), text);
+      assertEquals(new String(json.toJsonBytes(value), 
StandardCharsets.UTF_8), text);
+      assertEquals(json.fromJson(text, ArrayField.class).bytes, values[i]);
+      assertEquals(
+          json.fromJson(text.getBytes(StandardCharsets.UTF_8), 
ArrayField.class).bytes, values[i]);
+      assertEquals(
+          json.fromJson("{\"ignored\":\"汉\",\"bytes\":" + encoded[i] + "}", 
ArrayField.class).bytes,
+          values[i]);
+    }
+    assertNull(json.fromJson("{\"bytes\":null}", ArrayField.class).bytes);
+    for (String encodedValue : new String[] {"[128]", "[-129]", "[null]", 
"\"AQ==\""}) {
+      assertThrows(
+          ForyJsonException.class,
+          () -> json.fromJson("{\"bytes\":" + encodedValue + "}", 
ArrayField.class));
+    }
+    assertGeneratedWhenSupported(json, ArrayField.class, codegenEnabled());
+  }
+
+  @Test
+  public void arrayGetter() {
+    ForyJson json = newJson();
+    ArrayGetter value = new ArrayGetter();
+    value.bytes = new byte[] {1, -2};
+    assertEquals(json.toJson(value), "{\"bytes\":[1,-2]}");
+    assertEquals(json.fromJson("{\"bytes\":[1,-2]}", ArrayGetter.class).bytes, 
value.bytes);
+  }
+
+  @Test
+  public void conflictingFormats() {
+    assertThrows(ForyJsonException.class, () -> newJson().toJson(new 
ConflictingFormat()));
+  }
+
+  public static final class ArrayField {
+    @JsonByteArray(JsonByteArray.Format.ARRAY)
+    public byte[] bytes;
+  }
+
+  public static final class ArrayGetter {
+    private byte[] bytes;
+
+    @JsonByteArray(JsonByteArray.Format.ARRAY)
+    public byte[] getBytes() {
+      return bytes;
+    }
+
+    public void setBytes(byte[] bytes) {
+      this.bytes = bytes;
+    }
+  }
+
+  public static final class ConflictingFormat {
+    @JsonByteArray(JsonByteArray.Format.ARRAY)
+    public byte[] bytes = {1};
+
+    @JsonByteArray(JsonByteArray.Format.BASE64)
+    public byte[] getBytes() {
+      return bytes;
+    }
+  }
+
   @Test
   public void fieldRoundTrip() {
     ForyJson json = newJson();
@@ -151,10 +220,10 @@ public class JsonBase64AnnotationTest extends 
ForyJsonTestModels {
     }
     Class<?> type =
         compileRecordClass(
-            "JsonBase64Record",
+            "JsonByteArrayRecord",
             "package org.apache.fory.json.records;\n"
-                + "import org.apache.fory.json.annotation.JsonBase64;\n"
-                + "public record JsonBase64Record(@JsonBase64 byte[] bytes) 
{}\n");
+                + "import org.apache.fory.json.annotation.JsonByteArray;\n"
+                + "public record 
JsonByteArrayRecord(@JsonByteArray(JsonByteArray.Format.BASE64) byte[] bytes) 
{}\n");
     Object value = type.getConstructor(byte[].class).newInstance((Object) new 
byte[] {1, 2, 3});
     for (ForyJson json : new ForyJson[] {newJson(), 
newJsonBuilder().withFieldMode(true).build()}) {
       assertEquals(json.toJson(value), "{\"bytes\":\"AQID\"}");
@@ -199,13 +268,16 @@ public class JsonBase64AnnotationTest extends 
ForyJsonTestModels {
   }
 
   public static final class Base64Field {
-    @JsonBase64 public byte[] bytes;
+    @JsonByteArray(JsonByteArray.Format.BASE64)
+    public byte[] bytes;
   }
 
   @JsonPropertyOrder({"text", "bytes"})
   public static final class UnicodeBase64 {
     public String text;
-    @JsonBase64 public byte[] bytes;
+
+    @JsonByteArray(JsonByteArray.Format.BASE64)
+    public byte[] bytes;
   }
 
   public static final class Base64Getter {
@@ -217,7 +289,7 @@ public class JsonBase64AnnotationTest extends 
ForyJsonTestModels {
       this.bytes = bytes;
     }
 
-    @JsonBase64
+    @JsonByteArray(JsonByteArray.Format.BASE64)
     public byte[] getBytes() {
       return bytes;
     }
@@ -228,19 +300,20 @@ public class JsonBase64AnnotationTest extends 
ForyJsonTestModels {
   }
 
   public static final class Base64ReadOnly {
-    @JsonBase64
+    @JsonByteArray(JsonByteArray.Format.BASE64)
     @JsonIgnore(ignoreRead = false, ignoreWrite = true)
     public byte[] bytes;
   }
 
   public static final class Base64WriteOnly {
-    @JsonBase64
+    @JsonByteArray(JsonByteArray.Format.BASE64)
     @JsonIgnore(ignoreRead = true, ignoreWrite = false)
     public byte[] bytes;
   }
 
   public static final class PropertyListBase64 {
-    @JsonBase64 public final byte[] bytes;
+    @JsonByteArray(JsonByteArray.Format.BASE64)
+    public final byte[] bytes;
 
     @JsonCreator({"bytes"})
     public PropertyListBase64(byte[] bytes) {
@@ -249,7 +322,8 @@ public class JsonBase64AnnotationTest extends 
ForyJsonTestModels {
   }
 
   public static final class ParameterLocalBase64 {
-    @JsonBase64 public final byte[] bytes;
+    @JsonByteArray(JsonByteArray.Format.BASE64)
+    public final byte[] bytes;
 
     @JsonCreator
     public ParameterLocalBase64(@JsonProperty("bytes") byte[] bytes) {
@@ -258,7 +332,7 @@ public class JsonBase64AnnotationTest extends 
ForyJsonTestModels {
   }
 
   public static final class Base64Always {
-    @JsonBase64
+    @JsonByteArray(JsonByteArray.Format.BASE64)
     @JsonProperty(include = JsonProperty.Include.ALWAYS)
     public byte[] bytes;
   }
@@ -269,33 +343,41 @@ public class JsonBase64AnnotationTest extends 
ForyJsonTestModels {
   }
 
   public static final class NonBinaryBase64 {
-    @JsonBase64 public String value = "x";
+    @JsonByteArray(JsonByteArray.Format.BASE64)
+    public String value = "x";
   }
 
   public static final class StaticBase64 {
-    @JsonBase64 public static byte[] value = {1};
+    @JsonByteArray(JsonByteArray.Format.BASE64)
+    public static byte[] value = {1};
   }
 
   public static final class CodecBase64 {
-    @JsonBase64
+    @JsonByteArray(JsonByteArray.Format.BASE64)
     @JsonCodec(Base64ByteArrayCodec.class)
     public byte[] value = {1};
   }
 
   public static final class RawBase64 {
-    @JsonBase64 @JsonRawValue public byte[] value = {1};
+    @JsonByteArray(JsonByteArray.Format.BASE64)
+    @JsonRawValue
+    public byte[] value = {1};
   }
 
   public static final class IgnoredBase64 {
-    @JsonBase64 @JsonIgnore public byte[] value = {1};
+    @JsonByteArray(JsonByteArray.Format.BASE64)
+    @JsonIgnore
+    public byte[] value = {1};
   }
 
   public static final class AnyFieldBase64 {
-    @JsonBase64 @JsonAnyProperty public Map<String, byte[]> values;
+    @JsonByteArray(JsonByteArray.Format.BASE64)
+    @JsonAnyProperty
+    public Map<String, byte[]> values;
   }
 
   public static final class AnyGetterBase64 {
-    @JsonBase64
+    @JsonByteArray(JsonByteArray.Format.BASE64)
     @JsonAnyGetter
     public Map<String, byte[]> getValues() {
       return null;
diff --git 
a/java/fory-json/src/test/java/org/apache/fory/json/JsonContainerTest.java 
b/java/fory-json/src/test/java/org/apache/fory/json/JsonContainerTest.java
index 07ee115fd..3f68dbc2c 100644
--- a/java/fory-json/src/test/java/org/apache/fory/json/JsonContainerTest.java
+++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonContainerTest.java
@@ -25,6 +25,7 @@ import static 
org.apache.fory.json.JsonTestSupport.newUtf16Reader;
 import static org.apache.fory.json.JsonTestSupport.newUtf8Reader;
 import static org.testng.Assert.assertEquals;
 import static org.testng.Assert.assertNotEquals;
+import static org.testng.Assert.assertNull;
 import static org.testng.Assert.assertThrows;
 import static org.testng.Assert.assertTrue;
 
@@ -68,6 +69,7 @@ import java.util.concurrent.atomic.AtomicIntegerArray;
 import java.util.concurrent.atomic.AtomicLongArray;
 import java.util.concurrent.atomic.AtomicReferenceArray;
 import org.apache.fory.json.codec.ArrayCodec;
+import org.apache.fory.json.codec.JsonValueCodec;
 import org.apache.fory.json.codec.MapCodec;
 import org.apache.fory.json.codec.MapKeyCodec;
 import org.apache.fory.json.data.FastContainers;
@@ -128,9 +130,9 @@ public class JsonContainerTest extends ForyJsonTestModels {
   @Test
   public void unsignedArrayOverflow() {
     byte[] input = "[4294967295]".getBytes(StandardCharsets.UTF_8);
-    ArrayCodec<byte[]> uint8 =
+    JsonValueCodec<byte[]> uint8 =
         ArrayCodec.createUnsignedPrimitive(byte[].class, Types.UINT8_ARRAY, 
false);
-    ArrayCodec<short[]> uint16 =
+    JsonValueCodec<short[]> uint16 =
         ArrayCodec.createUnsignedPrimitive(short[].class, Types.UINT16_ARRAY, 
false);
 
     assertThrows(ForyJsonException.class, () -> 
uint8.readUtf8(newUtf8Reader(input)));
@@ -606,11 +608,27 @@ public class JsonContainerTest extends ForyJsonTestModels 
{
     assertEquals(
         json.fromJson("[true,false]".getBytes(StandardCharsets.UTF_8), 
boolean[].class),
         new boolean[] {true, false});
-    assertEquals(json.fromJson("[1,-2,3]", byte[].class), new byte[] {1, -2, 
3});
+    assertEquals(json.fromJson("\"Af4D\"", byte[].class), new byte[] {1, -2, 
3});
     assertEquals(json.fromJson("[\"a\",\"你\"]", char[].class), new char[] 
{'a', '你'});
     assertThrows(ForyJsonException.class, () -> json.fromJson("[1,null]", 
int[].class));
   }
 
+  @Test
+  public void byteArrayDefaultsToBase64() {
+    ForyJson json = newJson();
+    assertEquals(json.toJson(new byte[] {1, -2, 3}), "\"Af4D\"");
+    assertEquals(
+        new String(json.toJsonBytes(new byte[] {1, -2, 3}), 
StandardCharsets.UTF_8), "\"Af4D\"");
+    assertEquals(json.fromJson("\"Af4D\"", byte[].class), new byte[] {1, -2, 
3});
+    assertEquals(
+        json.fromJson("\"Af4D\"".getBytes(StandardCharsets.UTF_8), 
byte[].class),
+        new byte[] {1, -2, 3});
+    assertEquals(json.toJson(new byte[0]), "\"\"");
+    assertEquals(json.toJson(null, byte[].class), "null");
+    assertNull(json.fromJson("null", byte[].class));
+    assertThrows(ForyJsonException.class, () -> json.fromJson("[1,-2,3]", 
byte[].class));
+  }
+
   @Test
   public void readStringArrays() {
     ForyJson json = newJson();
diff --git 
a/java/fory-json/src/test/java/org/apache/fory/json/JsonGraphMemoryBudgetTest.java
 
b/java/fory-json/src/test/java/org/apache/fory/json/JsonGraphMemoryBudgetTest.java
index 687b425ad..6144f108a 100644
--- 
a/java/fory-json/src/test/java/org/apache/fory/json/JsonGraphMemoryBudgetTest.java
+++ 
b/java/fory-json/src/test/java/org/apache/fory/json/JsonGraphMemoryBudgetTest.java
@@ -38,6 +38,7 @@ import java.util.Set;
 import java.util.concurrent.atomic.AtomicReference;
 import java.util.concurrent.atomic.AtomicReferenceArray;
 import org.apache.fory.json.annotation.JsonAnyProperty;
+import org.apache.fory.json.annotation.JsonByteArray;
 import org.apache.fory.json.annotation.JsonCreator;
 import org.apache.fory.json.annotation.JsonSubTypes;
 import org.apache.fory.json.annotation.JsonUnwrapped;
@@ -174,7 +175,6 @@ public class JsonGraphMemoryBudgetTest extends 
ForyJsonTestModels {
     assertEquals(
         assertClassBudget("[true]", boolean[].class, headerBytes + Byte.BYTES),
         new boolean[] {true});
-    assertEquals(assertClassBudget("[1]", byte[].class, headerBytes + 
Byte.BYTES), new byte[] {1});
     assertEquals(
         assertClassBudget("[2]", short[].class, headerBytes + Short.BYTES), 
new short[] {2});
     assertEquals(assertClassBudget("[3]", int[].class, headerBytes + 
Integer.BYTES), new int[] {3});
@@ -193,6 +193,37 @@ public class JsonGraphMemoryBudgetTest extends 
ForyJsonTestModels {
         new int[] {7});
   }
 
+  @Test
+  public void byteArrayRepresentations() {
+    long arrayBytes = shallow(ArrayBytes.class) + 
GraphMemoryEstimates.objectArrayBytes() + 1;
+    assertEquals(
+        assertClassBudget("{\"bytes\":[1]}", ArrayBytes.class, 
arrayBytes).bytes, new byte[] {1});
+    assertEquals(
+        assertClassBytesBudget(
+                "{\"bytes\":[1]}".getBytes(StandardCharsets.UTF_8), 
ArrayBytes.class, arrayBytes)
+            .bytes,
+        new byte[] {1});
+    ForyJson binaryJson = jsonWithBudget(shallow(BinaryBytes.class));
+    for (String encoded : new String[] {"AQ==", "A\\u0051=="}) {
+      String input = "{\"bytes\":\"" + encoded + "\"}";
+      assertEquals(binaryJson.fromJson(input, BinaryBytes.class).bytes, new 
byte[] {1});
+      assertEquals(
+          binaryJson.fromJson(input.getBytes(StandardCharsets.UTF_8), 
BinaryBytes.class).bytes,
+          new byte[] {1});
+      assertEquals(jsonWithBudget(1).fromJson("\"" + encoded + "\"", 
byte[].class), new byte[] {1});
+    }
+  }
+
+  public static final class ArrayBytes {
+    @JsonByteArray(JsonByteArray.Format.ARRAY)
+    public byte[] bytes;
+  }
+
+  public static final class BinaryBytes {
+    @JsonByteArray(JsonByteArray.Format.BASE64)
+    public byte[] bytes;
+  }
+
   @Test
   public void primitiveArrayBatches() {
     int headerBytes = GraphMemoryEstimates.objectArrayBytes();
diff --git 
a/java/fory-json/src/test/java/org/apache/fory/json/JsonMixinTest.java 
b/java/fory-json/src/test/java/org/apache/fory/json/JsonMixinTest.java
index 04016bf19..6442962cd 100644
--- a/java/fory-json/src/test/java/org/apache/fory/json/JsonMixinTest.java
+++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonMixinTest.java
@@ -37,7 +37,7 @@ import java.util.UUID;
 import org.apache.fory.json.annotation.JsonAnyGetter;
 import org.apache.fory.json.annotation.JsonAnyProperty;
 import org.apache.fory.json.annotation.JsonAnySetter;
-import org.apache.fory.json.annotation.JsonBase64;
+import org.apache.fory.json.annotation.JsonByteArray;
 import org.apache.fory.json.annotation.JsonCodec;
 import org.apache.fory.json.annotation.JsonCreator;
 import org.apache.fory.json.annotation.JsonIgnore;
@@ -267,7 +267,7 @@ public class JsonMixinTest extends ForyJsonTestModels {
         
newJsonBuilder().registerMixin(RepresentationRemoveMixin.class).build();
     assertEquals(
         representation.toJson(new RepresentationRemoveTarget()),
-        "{\"name\":\"name\",\"raw\":\"1\",\"bytes\":[1],"
+        "{\"name\":\"name\",\"raw\":\"1\",\"bytes\":\"AQ==\","
             + "\"child\":{\"label\":\"kid\"},\"hidden\":7}");
 
     ForyJson anyField = 
newJsonBuilder().registerMixin(AnyFieldRemoveMixin.class).build();
@@ -437,7 +437,7 @@ public class JsonMixinTest extends ForyJsonTestModels {
                 + mixinName
                 + " {\n"
                 + "  @JsonProperty(\"display_name\") String name;\n"
-                + "  @JsonBase64 byte[] bytes;\n"
+                + "  @JsonByteArray(JsonByteArray.Format.BASE64) byte[] 
bytes;\n"
                 + "  "
                 + mixinName
                 + "(\n"
@@ -511,7 +511,9 @@ public class JsonMixinTest extends ForyJsonTestModels {
     String name;
 
     @JsonRawValue String body;
-    @JsonBase64 byte[] bytes;
+
+    @JsonByteArray(JsonByteArray.Format.BASE64)
+    byte[] bytes;
 
     @JsonUnwrapped(prefix = "child_")
     BasicChild child;
@@ -830,7 +832,9 @@ public class JsonMixinTest extends ForyJsonTestModels {
     public String name = "name";
 
     @JsonRawValue public String raw = "1";
-    @JsonBase64 public byte[] bytes = new byte[] {1};
+
+    @JsonByteArray(JsonByteArray.Format.ARRAY)
+    public byte[] bytes = new byte[] {1};
 
     @JsonUnwrapped(prefix = "child_")
     public BasicChild child = new BasicChild("kid");
@@ -846,7 +850,7 @@ public class JsonMixinTest extends ForyJsonTestModels {
     @JsonMixinRemove(JsonRawValue.class)
     String raw;
 
-    @JsonMixinRemove(JsonBase64.class)
+    @JsonMixinRemove(JsonByteArray.class)
     byte[] bytes;
 
     @JsonMixinRemove(JsonUnwrapped.class)
diff --git 
a/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java 
b/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java
index 14c422c4b..77bf09916 100644
--- a/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java
+++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java
@@ -1193,7 +1193,7 @@ public class JsonScalarTest extends ForyJsonTestModels {
     assertEquals(
         json.fromJson("[\"true\",\"false\"]".getBytes(StandardCharsets.UTF_8), 
boolean[].class),
         new boolean[] {true, false});
-    assertEquals(json.fromJson("[\"2\",\"3\"]", byte[].class), new byte[] {2, 
3});
+    assertEquals(json.fromJson("\"AgM=\"", byte[].class), new byte[] {2, 3});
     assertEquals(json.fromJson("[\"4\",\"5\"]", short[].class), new short[] 
{4, 5});
     assertEquals(json.fromJson("[\"6\",\"7\"]", int[].class), new int[] {6, 
7});
     assertEquals(
diff --git 
a/java/fory-json/src/test/java/org/apache/fory/json/JsonUnwrappedTest.java 
b/java/fory-json/src/test/java/org/apache/fory/json/JsonUnwrappedTest.java
index 94f345b61..2e1a629cf 100644
--- a/java/fory-json/src/test/java/org/apache/fory/json/JsonUnwrappedTest.java
+++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonUnwrappedTest.java
@@ -31,7 +31,7 @@ import java.util.Collections;
 import java.util.LinkedHashMap;
 import java.util.Map;
 import org.apache.fory.json.annotation.JsonAnyProperty;
-import org.apache.fory.json.annotation.JsonBase64;
+import org.apache.fory.json.annotation.JsonByteArray;
 import org.apache.fory.json.annotation.JsonCodec;
 import org.apache.fory.json.annotation.JsonCreator;
 import org.apache.fory.json.annotation.JsonIgnore;
@@ -801,7 +801,9 @@ public class JsonUnwrappedTest extends ForyJsonTestModels {
 
   public static class ValueRepresentationChild {
     @JsonRawValue public String raw;
-    @JsonBase64 public byte[] bytes;
+
+    @JsonByteArray(JsonByteArray.Format.BASE64)
+    public byte[] bytes;
   }
 
   public static class ValueObjectParent {
diff --git 
a/kotlin/fory-json-kotlin-ksp/src/main/kotlin/org/apache/fory/json/kotlin/ksp/KspModelBuilder.kt
 
b/kotlin/fory-json-kotlin-ksp/src/main/kotlin/org/apache/fory/json/kotlin/ksp/KspModelBuilder.kt
index 6e89bb7c7..2a1b03c41 100644
--- 
a/kotlin/fory-json-kotlin-ksp/src/main/kotlin/org/apache/fory/json/kotlin/ksp/KspModelBuilder.kt
+++ 
b/kotlin/fory-json-kotlin-ksp/src/main/kotlin/org/apache/fory/json/kotlin/ksp/KspModelBuilder.kt
@@ -47,7 +47,7 @@ internal const val JSON_MIXIN: String = 
"org.apache.fory.json.annotation.JsonMix
 internal const val JSON_SUB_TYPES: String = 
"org.apache.fory.json.annotation.JsonSubTypes"
 private const val JSON_SUB_TYPE = 
"org.apache.fory.json.annotation.JsonSubTypes.Type"
 private const val JSON_CODEC = "org.apache.fory.json.annotation.JsonCodec"
-private const val JSON_BASE64 = "org.apache.fory.json.annotation.JsonBase64"
+private const val JSON_BYTE_ARRAY = 
"org.apache.fory.json.annotation.JsonByteArray"
 private const val JSON_ANY_SETTER = 
"org.apache.fory.json.annotation.JsonAnySetter"
 private const val JSON_VALIDATOR = 
"org.apache.fory.json.annotation.JsonValidator"
 private const val JSON_CREATOR = "org.apache.fory.json.annotation.JsonCreator"
@@ -1147,7 +1147,14 @@ internal class KspModelBuilder(
     result.annotations += name
     when (name) {
       JSON_CODEC -> collectCodecAnnotation(annotation, result.codecs)
-      JSON_BASE64 -> result.codecs += BASE64_CODEC
+      JSON_BYTE_ARRAY -> {
+        val format =
+          annotation.arguments.first { it.name?.asString() == "value" }.value 
as KSClassDeclaration
+        result.codecs +=
+          if (format.simpleName.asString() == "ARRAY")
+            "org.apache.fory.json.codec.ArrayCodec\$SignedByteArrayCodec"
+          else BASE64_CODEC
+      }
       JSON_SUB_TYPES -> collectSubtypeTypes(annotation, result.types)
       else ->
         annotation.arguments.forEach { argument -> 
collectTypeValue(argument.value, result.types) }


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

Reply via email to