You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@fory.apache.org by ch...@apache.org on 2026/09/21 13:34:47 UTC

(fory) branch main updated: feat(json): add global byte array formats and Base16 support (#4075)

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 99fd6b3a4 feat(json): add global byte array formats and Base16 support (#4075)
99fd6b3a4 is described below

commit 99fd6b3a4d23ca78f99070d3fe8555bb8fa48afa
Author: Shawn Yang <sh...@gmail.com>
AuthorDate: Mon Sep 21 21:34:41 2026 +0800

    feat(json): add global byte array formats and Base16 support (#4075)
    
    ## 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
---
 .agents/languages/java.md                          |   8 +-
 .../json/byte-array-formats-results.json           | 625 +++++++++++++++++++++
 docs/benchmarks/json/byte-array-formats.md         | 105 ++++
 docs/json/annotations.md                           |  16 +-
 docs/json/graalvm.md                               |   6 +-
 docs/json/kotlin.md                                |  18 +
 docs/json/object-mapping.md                        |  50 +-
 docs/json/scala.md                                 |  19 +
 docs/json/security.md                              |   2 +-
 .../org/apache/fory/graalvm/ForyJsonExample.java   |  63 +++
 .../kotlin/json/corpus/PlatformCorpusChecks.kt     |   2 +
 .../kotlin/json/corpus/PlatformModels.kt           |   2 +
 .../kotlin/json/corpus/KspRetentionResourceTest.kt |   1 +
 .../annotation/processing/JsonTypeProcessor.java   |   4 +-
 .../processing/JsonTypeProcessorTest.java          |  53 ++
 .../java/org/apache/fory/json/ForyJsonBuilder.java |  19 +
 .../main/java/org/apache/fory/json/JsonConfig.java |   9 +
 .../apache/fory/json/annotation/JsonByteArray.java |   7 +-
 .../org/apache/fory/json/codec/ArrayCodec.java     | 116 ++--
 .../fory/json/codec/Base16ByteArrayCodec.java      |  66 +++
 .../apache/fory/json/codec/ObjectCodecBuilder.java |   4 +-
 .../org/apache/fory/json/reader/JsonReader.java    |  75 +++
 .../apache/fory/json/reader/Latin1JsonReader.java  |   5 +
 .../apache/fory/json/reader/Utf8JsonReader.java    |  13 +-
 .../json/resolver/GeneratedCodecKeyBuilder.java    |   2 +
 .../fory/json/resolver/JsonSharedRegistry.java     |   9 +
 .../fory/json/resolver/JsonTypeResolver.java       |   6 +-
 .../org/apache/fory/json/writer/JsonWriter.java    |  16 +
 .../apache/fory/json/writer/StringJsonWriter.java  |  53 ++
 .../apache/fory/json/writer/Utf8JsonWriter.java    |  28 +
 .../apache/fory/json/ForyJsonGraalVMFeature.java   |   5 +-
 .../fory-json/native-image.properties              |   1 +
 .../json/ForyJsonGraalVMFeatureJarVerifier.java    |   1 +
 .../apache/fory/json/JsonAsyncCompilationTest.java |   2 +
 .../fory/json/JsonByteArrayAnnotationTest.java     | 236 ++++++++
 .../org/apache/fory/json/JsonContainerTest.java    |  25 +
 .../java/org/apache/fory/json/JsonScalarTest.java  |  30 +
 .../java/org/apache/fory/json/JsonTestSupport.java |   2 +
 .../apache/fory/json/kotlin/ksp/KspModelBuilder.kt |   8 +-
 .../fory/json/kotlin/KotlinBuiltInCodecsTest.kt    |  53 ++
 .../apache/fory/json/scala/ScalaJsonSuite.scala    |  47 +-
 41 files changed, 1730 insertions(+), 82 deletions(-)

diff --git a/.agents/languages/java.md b/.agents/languages/java.md
index deaa98a2e..f54e4cf1f 100644
--- a/.agents/languages/java.md
+++ b/.agents/languages/java.md
@@ -107,11 +107,13 @@ 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.
+- Fory JSON `byte[]` defaults to a Base64 string; `byteArrayFormat` can select BASE64, BASE16,
+  or signed ARRAY globally. `@JsonByteArray` overrides that default 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.
+  Base64 and Base16 values remain binary leaves outside that budget. Resolve the format during
+  codec selection, not on each write; semantic unsigned arrays retain their module-owned format.
 - 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/benchmarks/json/byte-array-formats-results.json b/docs/benchmarks/json/byte-array-formats-results.json
new file mode 100644
index 000000000..ca2f84727
--- /dev/null
+++ b/docs/benchmarks/json/byte-array-formats-results.json
@@ -0,0 +1,625 @@
+{
+  "environment": {
+    "cpu": "Apple M4 Pro",
+    "logical_cpus": 12,
+    "os": "macOS 15.7.2 arm64",
+    "jdk": "OpenJDK 25.0.3",
+    "jmh": "1.37"
+  },
+  "source": {
+    "fory": "13739263ebf31af5fc962184aaccf2833f8ffc9a",
+    "fory_baseline": "e1d2f9d67800901392755b1133968d448ab076b8",
+    "jsoniter_scala_base": "84cb277f2c2a16b4018bc61fdeec3eed62252a1e",
+    "jsoniter_scala_adapters": "71b2ba16f1d5261e2be4e98d5dfe4be452d0e136",
+    "jsoniter_scala_version": "2.40.2-SNAPSHOT",
+    "benchmark_scala": "3.9.0",
+    "fory_scala": "3.3.8"
+  },
+  "settings": {
+    "threads": 1,
+    "forks_per_run": 1,
+    "warmup_iterations": 3,
+    "measurement_iterations": 5,
+    "iteration_seconds": 1,
+    "jvm_args": ["-Xms1g", "-Xmx1g", "-XX:+UseParallelGC"],
+    "profiler": "gc"
+  },
+  "results": [
+    {
+      "case": "ArrayOfBytesReading",
+      "size": 512,
+      "pairs": [
+        {
+          "pair": 1,
+          "order": ["jsoniter_scala", "fory"],
+          "fory": {
+            "throughput_ops_s": 718561.6343142863,
+            "iterations_ops_s": [
+              720966.2774700475, 722148.059950197, 723968.2127171038,
+              699675.4454108089, 726050.1760232744
+            ],
+            "allocation_bytes_op": 528.0081562652304,
+            "artifact_sha256": {
+              "fory_json": "a60312c784247481c49a6a7a2bf128979fdba070c305a0e1f95966673237883b",
+              "fory_scala": "69bce71e74891f86f0a2b1a607b2a86743955e5378386dbc3ab72e64c30f62cd",
+              "fory_core": "1d743236d0529f1e0c082bba9abc36bf502167add84905441ad917f7143b5b9a",
+              "benchmark": "606d03f7f6fb4bbf217b3e2d30ae82054d702aa937148328b1987191bd63e3b4"
+            }
+          },
+          "jsoniter_scala": {
+            "throughput_ops_s": 644154.4427726491,
+            "iterations_ops_s": [
+              642739.6225877892, 656549.9366009792, 647274.0194844017,
+              629323.8870815604, 644884.7481085152
+            ],
+            "allocation_bytes_op": 1128.0091134734967,
+            "artifact_sha256": {
+              "fory_json": "a60312c784247481c49a6a7a2bf128979fdba070c305a0e1f95966673237883b",
+              "fory_scala": "69bce71e74891f86f0a2b1a607b2a86743955e5378386dbc3ab72e64c30f62cd",
+              "fory_core": "1d743236d0529f1e0c082bba9abc36bf502167add84905441ad917f7143b5b9a",
+              "benchmark": "606d03f7f6fb4bbf217b3e2d30ae82054d702aa937148328b1987191bd63e3b4"
+            }
+          },
+          "throughput_ratio": 1.1155114155874863
+        },
+        {
+          "pair": 2,
+          "order": ["fory", "jsoniter_scala"],
+          "fory": {
+            "throughput_ops_s": 712768.7484642493,
+            "iterations_ops_s": [
+              715809.6255148505, 712322.2180004838, 719427.3859759014,
+              706451.1582493099, 709833.3545807011
+            ],
+            "allocation_bytes_op": 528.0082231131366,
+            "artifact_sha256": {
+              "fory_json": "a60312c784247481c49a6a7a2bf128979fdba070c305a0e1f95966673237883b",
+              "fory_scala": "69bce71e74891f86f0a2b1a607b2a86743955e5378386dbc3ab72e64c30f62cd",
+              "fory_core": "1d743236d0529f1e0c082bba9abc36bf502167add84905441ad917f7143b5b9a",
+              "benchmark": "606d03f7f6fb4bbf217b3e2d30ae82054d702aa937148328b1987191bd63e3b4"
+            }
+          },
+          "jsoniter_scala": {
+            "throughput_ops_s": 646910.5376279108,
+            "iterations_ops_s": [
+              653642.6650007655, 648076.5626362266, 639063.9666087512,
+              648406.7871787745, 645362.7067150369
+            ],
+            "allocation_bytes_op": 1128.0090703040241,
+            "artifact_sha256": {
+              "fory_json": "a60312c784247481c49a6a7a2bf128979fdba070c305a0e1f95966673237883b",
+              "fory_scala": "69bce71e74891f86f0a2b1a607b2a86743955e5378386dbc3ab72e64c30f62cd",
+              "fory_core": "1d743236d0529f1e0c082bba9abc36bf502167add84905441ad917f7143b5b9a",
+              "benchmark": "606d03f7f6fb4bbf217b3e2d30ae82054d702aa937148328b1987191bd63e3b4"
+            }
+          },
+          "throughput_ratio": 1.1018042016718836
+        },
+        {
+          "pair": 3,
+          "order": ["jsoniter_scala", "fory"],
+          "fory": {
+            "throughput_ops_s": 713855.8384418925,
+            "iterations_ops_s": [
+              713930.6941047948, 699719.8311706063, 713989.776513808,
+              721117.7876197997, 720521.1028004538
+            ],
+            "allocation_bytes_op": 528.0082200551103,
+            "artifact_sha256": {
+              "fory_json": "a60312c784247481c49a6a7a2bf128979fdba070c305a0e1f95966673237883b",
+              "fory_scala": "69bce71e74891f86f0a2b1a607b2a86743955e5378386dbc3ab72e64c30f62cd",
+              "fory_core": "1d743236d0529f1e0c082bba9abc36bf502167add84905441ad917f7143b5b9a",
+              "benchmark": "606d03f7f6fb4bbf217b3e2d30ae82054d702aa937148328b1987191bd63e3b4"
+            }
+          },
+          "jsoniter_scala": {
+            "throughput_ops_s": 648694.3313125791,
+            "iterations_ops_s": [
+              640046.1536157951, 651947.475615739, 650393.8228952556,
+              649302.6479094238, 651781.5565266822
+            ],
+            "allocation_bytes_op": 1128.0090437194845,
+            "artifact_sha256": {
+              "fory_json": "a60312c784247481c49a6a7a2bf128979fdba070c305a0e1f95966673237883b",
+              "fory_scala": "69bce71e74891f86f0a2b1a607b2a86743955e5378386dbc3ab72e64c30f62cd",
+              "fory_core": "1d743236d0529f1e0c082bba9abc36bf502167add84905441ad917f7143b5b9a",
+              "benchmark": "606d03f7f6fb4bbf217b3e2d30ae82054d702aa937148328b1987191bd63e3b4"
+            }
+          },
+          "throughput_ratio": 1.100450249037115
+        }
+      ],
+      "median_fory_ops_s": 713855.8384418925,
+      "median_jsoniter_ops_s": 646910.5376279108,
+      "median_paired_ratio": 1.1018042016718836,
+      "minimum_paired_ratio": 1.100450249037115,
+      "maximum_paired_ratio": 1.1155114155874863,
+      "geometric_paired_ratio": 1.1059010882137206
+    },
+    {
+      "case": "ArrayOfBytesWriting",
+      "size": 512,
+      "pairs": [
+        {
+          "pair": 1,
+          "order": ["jsoniter_scala", "fory"],
+          "fory": {
+            "throughput_ops_s": 3350580.997475249,
+            "iterations_ops_s": [
+              3354256.676965812, 3350235.8910908336, 3370583.284923328,
+              3343149.3809342943, 3334679.7534619793
+            ],
+            "allocation_bytes_op": 1888.0017545395815,
+            "artifact_sha256": {
+              "fory_json": "a60312c784247481c49a6a7a2bf128979fdba070c305a0e1f95966673237883b",
+              "fory_scala": "69bce71e74891f86f0a2b1a607b2a86743955e5378386dbc3ab72e64c30f62cd",
+              "fory_core": "1d743236d0529f1e0c082bba9abc36bf502167add84905441ad917f7143b5b9a",
+              "benchmark": "606d03f7f6fb4bbf217b3e2d30ae82054d702aa937148328b1987191bd63e3b4"
+            }
+          },
+          "jsoniter_scala": {
+            "throughput_ops_s": 1534400.3964775414,
+            "iterations_ops_s": [
+              1555002.3973013056, 1551437.3792732242, 1483784.9963905278,
+              1539026.116856106, 1542751.092566543
+            ],
+            "allocation_bytes_op": 1888.003826423861,
+            "artifact_sha256": {
+              "fory_json": "a60312c784247481c49a6a7a2bf128979fdba070c305a0e1f95966673237883b",
+              "fory_scala": "69bce71e74891f86f0a2b1a607b2a86743955e5378386dbc3ab72e64c30f62cd",
+              "fory_core": "1d743236d0529f1e0c082bba9abc36bf502167add84905441ad917f7143b5b9a",
+              "benchmark": "606d03f7f6fb4bbf217b3e2d30ae82054d702aa937148328b1987191bd63e3b4"
+            }
+          },
+          "throughput_ratio": 2.1836419002282828
+        },
+        {
+          "pair": 2,
+          "order": ["fory", "jsoniter_scala"],
+          "fory": {
+            "throughput_ops_s": 3355366.2425028966,
+            "iterations_ops_s": [
+              3371208.1427841824, 3370052.5561299818, 3367852.548591301,
+              3335320.999305273, 3332396.965703745
+            ],
+            "allocation_bytes_op": 1888.0017485208652,
+            "artifact_sha256": {
+              "fory_json": "a60312c784247481c49a6a7a2bf128979fdba070c305a0e1f95966673237883b",
+              "fory_scala": "69bce71e74891f86f0a2b1a607b2a86743955e5378386dbc3ab72e64c30f62cd",
+              "fory_core": "1d743236d0529f1e0c082bba9abc36bf502167add84905441ad917f7143b5b9a",
+              "benchmark": "606d03f7f6fb4bbf217b3e2d30ae82054d702aa937148328b1987191bd63e3b4"
+            }
+          },
+          "jsoniter_scala": {
+            "throughput_ops_s": 1539684.6759449516,
+            "iterations_ops_s": [
+              1530588.7989645593, 1536345.1128779924, 1550446.53671775,
+              1520990.2236222902, 1560052.7075421656
+            ],
+            "allocation_bytes_op": 1888.0038163834627,
+            "artifact_sha256": {
+              "fory_json": "a60312c784247481c49a6a7a2bf128979fdba070c305a0e1f95966673237883b",
+              "fory_scala": "69bce71e74891f86f0a2b1a607b2a86743955e5378386dbc3ab72e64c30f62cd",
+              "fory_core": "1d743236d0529f1e0c082bba9abc36bf502167add84905441ad917f7143b5b9a",
+              "benchmark": "606d03f7f6fb4bbf217b3e2d30ae82054d702aa937148328b1987191bd63e3b4"
+            }
+          },
+          "throughput_ratio": 2.179255463748514
+        },
+        {
+          "pair": 3,
+          "order": ["jsoniter_scala", "fory"],
+          "fory": {
+            "throughput_ops_s": 3328563.302552803,
+            "iterations_ops_s": [
+              3320520.682048541, 3331924.359155337, 3319451.2998794424,
+              3332810.7688661655, 3338109.40281453
+            ],
+            "allocation_bytes_op": 1888.0017662799612,
+            "artifact_sha256": {
+              "fory_json": "a60312c784247481c49a6a7a2bf128979fdba070c305a0e1f95966673237883b",
+              "fory_scala": "69bce71e74891f86f0a2b1a607b2a86743955e5378386dbc3ab72e64c30f62cd",
+              "fory_core": "1d743236d0529f1e0c082bba9abc36bf502167add84905441ad917f7143b5b9a",
+              "benchmark": "606d03f7f6fb4bbf217b3e2d30ae82054d702aa937148328b1987191bd63e3b4"
+            }
+          },
+          "jsoniter_scala": {
+            "throughput_ops_s": 1540486.9964760381,
+            "iterations_ops_s": [
+              1537960.3534940826, 1560478.5204790654, 1537766.2635790894,
+              1527368.4693392226, 1538861.3754887313
+            ],
+            "allocation_bytes_op": 1888.0038109181107,
+            "artifact_sha256": {
+              "fory_json": "a60312c784247481c49a6a7a2bf128979fdba070c305a0e1f95966673237883b",
+              "fory_scala": "69bce71e74891f86f0a2b1a607b2a86743955e5378386dbc3ab72e64c30f62cd",
+              "fory_core": "1d743236d0529f1e0c082bba9abc36bf502167add84905441ad917f7143b5b9a",
+              "benchmark": "606d03f7f6fb4bbf217b3e2d30ae82054d702aa937148328b1987191bd63e3b4"
+            }
+          },
+          "throughput_ratio": 2.1607214537786446
+        }
+      ],
+      "median_fory_ops_s": 3350580.997475249,
+      "median_jsoniter_ops_s": 1539684.6759449516,
+      "median_paired_ratio": 2.179255463748514,
+      "minimum_paired_ratio": 2.1607214537786446,
+      "maximum_paired_ratio": 2.1836419002282828,
+      "geometric_paired_ratio": 2.174516874590141
+    },
+    {
+      "case": "Base16Reading",
+      "size": 512,
+      "pairs": [
+        {
+          "pair": 1,
+          "order": ["jsoniter_scala", "fory"],
+          "fory": {
+            "throughput_ops_s": 3359808.747011424,
+            "iterations_ops_s": [
+              3170890.4021431804, 2835783.848635741, 3599267.745232154,
+              3600313.8419093373, 3592787.8971367073
+            ],
+            "allocation_bytes_op": 528.0017648087484,
+            "artifact_sha256": {
+              "fory_json": "a60312c784247481c49a6a7a2bf128979fdba070c305a0e1f95966673237883b",
+              "fory_scala": "69bce71e74891f86f0a2b1a607b2a86743955e5378386dbc3ab72e64c30f62cd",
+              "fory_core": "1d743236d0529f1e0c082bba9abc36bf502167add84905441ad917f7143b5b9a",
+              "benchmark": "606d03f7f6fb4bbf217b3e2d30ae82054d702aa937148328b1987191bd63e3b4"
+            }
+          },
+          "jsoniter_scala": {
+            "throughput_ops_s": 2914929.3631949537,
+            "iterations_ops_s": [
+              2894217.0461341827, 2909705.468348089, 2928398.5845325943,
+              2897313.7225826643, 2945011.9943772373
+            ],
+            "allocation_bytes_op": 528.0020149494615,
+            "artifact_sha256": {
+              "fory_json": "a60312c784247481c49a6a7a2bf128979fdba070c305a0e1f95966673237883b",
+              "fory_scala": "69bce71e74891f86f0a2b1a607b2a86743955e5378386dbc3ab72e64c30f62cd",
+              "fory_core": "1d743236d0529f1e0c082bba9abc36bf502167add84905441ad917f7143b5b9a",
+              "benchmark": "606d03f7f6fb4bbf217b3e2d30ae82054d702aa937148328b1987191bd63e3b4"
+            }
+          },
+          "throughput_ratio": 1.1526209826672622
+        },
+        {
+          "pair": 2,
+          "order": ["fory", "jsoniter_scala"],
+          "fory": {
+            "throughput_ops_s": 3585325.1970511647,
+            "iterations_ops_s": [
+              3606779.6047478, 3620212.096465545, 3585192.9164911993,
+              3573313.8467803146, 3541127.5207709647
+            ],
+            "allocation_bytes_op": 528.0016362855811,
+            "artifact_sha256": {
+              "fory_json": "a60312c784247481c49a6a7a2bf128979fdba070c305a0e1f95966673237883b",
+              "fory_scala": "69bce71e74891f86f0a2b1a607b2a86743955e5378386dbc3ab72e64c30f62cd",
+              "fory_core": "1d743236d0529f1e0c082bba9abc36bf502167add84905441ad917f7143b5b9a",
+              "benchmark": "606d03f7f6fb4bbf217b3e2d30ae82054d702aa937148328b1987191bd63e3b4"
+            }
+          },
+          "jsoniter_scala": {
+            "throughput_ops_s": 2873552.418737786,
+            "iterations_ops_s": [
+              2761830.775159276, 2916270.0082977437, 2909785.4844934223,
+              2912280.347573207, 2867595.47816528
+            ],
+            "allocation_bytes_op": 528.0020441965414,
+            "artifact_sha256": {
+              "fory_json": "a60312c784247481c49a6a7a2bf128979fdba070c305a0e1f95966673237883b",
+              "fory_scala": "69bce71e74891f86f0a2b1a607b2a86743955e5378386dbc3ab72e64c30f62cd",
+              "fory_core": "1d743236d0529f1e0c082bba9abc36bf502167add84905441ad917f7143b5b9a",
+              "benchmark": "606d03f7f6fb4bbf217b3e2d30ae82054d702aa937148328b1987191bd63e3b4"
+            }
+          },
+          "throughput_ratio": 1.2476978577707751
+        },
+        {
+          "pair": 3,
+          "order": ["jsoniter_scala", "fory"],
+          "fory": {
+            "throughput_ops_s": 3603439.30158346,
+            "iterations_ops_s": [
+              3626219.544144616, 3602839.8185009575, 3602282.722974174,
+              3598315.7871311214, 3587538.6351664294
+            ],
+            "allocation_bytes_op": 528.0016283749771,
+            "artifact_sha256": {
+              "fory_json": "a60312c784247481c49a6a7a2bf128979fdba070c305a0e1f95966673237883b",
+              "fory_scala": "69bce71e74891f86f0a2b1a607b2a86743955e5378386dbc3ab72e64c30f62cd",
+              "fory_core": "1d743236d0529f1e0c082bba9abc36bf502167add84905441ad917f7143b5b9a",
+              "benchmark": "606d03f7f6fb4bbf217b3e2d30ae82054d702aa937148328b1987191bd63e3b4"
+            }
+          },
+          "jsoniter_scala": {
+            "throughput_ops_s": 2949382.2375222584,
+            "iterations_ops_s": [
+              2947324.1011699555, 2941788.9030553037, 2943400.2053664513,
+              2947093.167103884, 2967304.8109156964
+            ],
+            "allocation_bytes_op": 528.001988912211,
+            "artifact_sha256": {
+              "fory_json": "a60312c784247481c49a6a7a2bf128979fdba070c305a0e1f95966673237883b",
+              "fory_scala": "69bce71e74891f86f0a2b1a607b2a86743955e5378386dbc3ab72e64c30f62cd",
+              "fory_core": "1d743236d0529f1e0c082bba9abc36bf502167add84905441ad917f7143b5b9a",
+              "benchmark": "606d03f7f6fb4bbf217b3e2d30ae82054d702aa937148328b1987191bd63e3b4"
+            }
+          },
+          "throughput_ratio": 1.2217606981354399
+        }
+      ],
+      "median_fory_ops_s": 3585325.1970511647,
+      "median_jsoniter_ops_s": 2914929.3631949537,
+      "median_paired_ratio": 1.2217606981354399,
+      "minimum_paired_ratio": 1.1526209826672622,
+      "maximum_paired_ratio": 1.2476978577707751,
+      "geometric_paired_ratio": 1.2066853323390903
+    },
+    {
+      "case": "Base16Writing",
+      "size": 512,
+      "pairs": [
+        {
+          "pair": 1,
+          "order": ["jsoniter_scala", "fory"],
+          "fory": {
+            "throughput_ops_s": 9639285.749072284,
+            "iterations_ops_s": [
+              9720176.667163966, 9484401.322246242, 9672723.887067826,
+              9634169.371241517, 9684957.497641867
+            ],
+            "allocation_bytes_op": 1048.0006091523976,
+            "artifact_sha256": {
+              "fory_json": "a60312c784247481c49a6a7a2bf128979fdba070c305a0e1f95966673237883b",
+              "fory_scala": "69bce71e74891f86f0a2b1a607b2a86743955e5378386dbc3ab72e64c30f62cd",
+              "fory_core": "1d743236d0529f1e0c082bba9abc36bf502167add84905441ad917f7143b5b9a",
+              "benchmark": "606d03f7f6fb4bbf217b3e2d30ae82054d702aa937148328b1987191bd63e3b4"
+            }
+          },
+          "jsoniter_scala": {
+            "throughput_ops_s": 8472009.567249341,
+            "iterations_ops_s": [
+              8479680.230866075, 8457165.337729625, 8507324.055039847,
+              8446205.415500265, 8469672.797110893
+            ],
+            "allocation_bytes_op": 1048.0006932682013,
+            "artifact_sha256": {
+              "fory_json": "a60312c784247481c49a6a7a2bf128979fdba070c305a0e1f95966673237883b",
+              "fory_scala": "69bce71e74891f86f0a2b1a607b2a86743955e5378386dbc3ab72e64c30f62cd",
+              "fory_core": "1d743236d0529f1e0c082bba9abc36bf502167add84905441ad917f7143b5b9a",
+              "benchmark": "606d03f7f6fb4bbf217b3e2d30ae82054d702aa937148328b1987191bd63e3b4"
+            }
+          },
+          "throughput_ratio": 1.137780319127039
+        },
+        {
+          "pair": 2,
+          "order": ["fory", "jsoniter_scala"],
+          "fory": {
+            "throughput_ops_s": 9790192.385472855,
+            "iterations_ops_s": [
+              9729910.203410858, 9798489.073830001, 9801086.328943448,
+              9821711.761606097, 9799764.559573878
+            ],
+            "allocation_bytes_op": 1048.0005986565332,
+            "artifact_sha256": {
+              "fory_json": "a60312c784247481c49a6a7a2bf128979fdba070c305a0e1f95966673237883b",
+              "fory_scala": "69bce71e74891f86f0a2b1a607b2a86743955e5378386dbc3ab72e64c30f62cd",
+              "fory_core": "1d743236d0529f1e0c082bba9abc36bf502167add84905441ad917f7143b5b9a",
+              "benchmark": "606d03f7f6fb4bbf217b3e2d30ae82054d702aa937148328b1987191bd63e3b4"
+            }
+          },
+          "jsoniter_scala": {
+            "throughput_ops_s": 8444372.230910677,
+            "iterations_ops_s": [
+              8454419.247091914, 8498853.198057437, 8439041.593527095,
+              8413092.78031926, 8416454.335557679
+            ],
+            "allocation_bytes_op": 1048.0006945067705,
+            "artifact_sha256": {
+              "fory_json": "a60312c784247481c49a6a7a2bf128979fdba070c305a0e1f95966673237883b",
+              "fory_scala": "69bce71e74891f86f0a2b1a607b2a86743955e5378386dbc3ab72e64c30f62cd",
+              "fory_core": "1d743236d0529f1e0c082bba9abc36bf502167add84905441ad917f7143b5b9a",
+              "benchmark": "606d03f7f6fb4bbf217b3e2d30ae82054d702aa937148328b1987191bd63e3b4"
+            }
+          },
+          "throughput_ratio": 1.159374802266034
+        },
+        {
+          "pair": 3,
+          "order": ["jsoniter_scala", "fory"],
+          "fory": {
+            "throughput_ops_s": 9627140.75202604,
+            "iterations_ops_s": [
+              9571136.694928922, 9670291.685970405, 9650637.061303275,
+              9649359.818529114, 9594278.499398485
+            ],
+            "allocation_bytes_op": 1048.0006093169627,
+            "artifact_sha256": {
+              "fory_json": "a60312c784247481c49a6a7a2bf128979fdba070c305a0e1f95966673237883b",
+              "fory_scala": "69bce71e74891f86f0a2b1a607b2a86743955e5378386dbc3ab72e64c30f62cd",
+              "fory_core": "1d743236d0529f1e0c082bba9abc36bf502167add84905441ad917f7143b5b9a",
+              "benchmark": "606d03f7f6fb4bbf217b3e2d30ae82054d702aa937148328b1987191bd63e3b4"
+            }
+          },
+          "jsoniter_scala": {
+            "throughput_ops_s": 8299340.122327629,
+            "iterations_ops_s": [
+              8304739.0318363365, 8098181.555887745, 8329959.2642635,
+              8378818.139460061, 8385002.620190503
+            ],
+            "allocation_bytes_op": 1048.0007070897,
+            "artifact_sha256": {
+              "fory_json": "a60312c784247481c49a6a7a2bf128979fdba070c305a0e1f95966673237883b",
+              "fory_scala": "69bce71e74891f86f0a2b1a607b2a86743955e5378386dbc3ab72e64c30f62cd",
+              "fory_core": "1d743236d0529f1e0c082bba9abc36bf502167add84905441ad917f7143b5b9a",
+              "benchmark": "606d03f7f6fb4bbf217b3e2d30ae82054d702aa937148328b1987191bd63e3b4"
+            }
+          },
+          "throughput_ratio": 1.1599886991167216
+        }
+      ],
+      "median_fory_ops_s": 9639285.749072284,
+      "median_jsoniter_ops_s": 8444372.230910677,
+      "median_paired_ratio": 1.159374802266034,
+      "minimum_paired_ratio": 1.137780319127039,
+      "maximum_paired_ratio": 1.1599886991167216,
+      "geometric_paired_ratio": 1.1523347999694993
+    }
+  ],
+  "numeric_read_development_pairs": [
+    {
+      "pair": 1,
+      "measurement_iterations": 3,
+      "baseline": {
+        "throughput_ops_s": 532673.8573387515,
+        "iterations_ops_s": [
+          525513.9417793049, 542316.8084152596, 530190.8218216901
+        ],
+        "allocation_bytes_op": 1656.0110539563302,
+        "artifact_sha256": {
+          "fory_json": "0b175646200572bd67817e5ff6350ca9b41ecf8d9c5fedb46ef90677fe335014",
+          "fory_scala": "69bce71e74891f86f0a2b1a607b2a86743955e5378386dbc3ab72e64c30f62cd",
+          "fory_core": "1d743236d0529f1e0c082bba9abc36bf502167add84905441ad917f7143b5b9a",
+          "benchmark": "606d03f7f6fb4bbf217b3e2d30ae82054d702aa937148328b1987191bd63e3b4"
+        }
+      },
+      "fory": {
+        "throughput_ops_s": 688445.4281281951,
+        "iterations_ops_s": [
+          661077.1182975627, 700984.16167637, 703275.0044106527
+        ],
+        "allocation_bytes_op": 528.0085360717871,
+        "artifact_sha256": {
+          "fory_json": "3dc8c8cce674855b39faca03156bf57048bb28d1503513cd5d5a85c6935ea696",
+          "fory_scala": "69bce71e74891f86f0a2b1a607b2a86743955e5378386dbc3ab72e64c30f62cd",
+          "fory_core": "1d743236d0529f1e0c082bba9abc36bf502167add84905441ad917f7143b5b9a",
+          "benchmark": "606d03f7f6fb4bbf217b3e2d30ae82054d702aa937148328b1987191bd63e3b4"
+        }
+      },
+      "jsoniter_scala": {
+        "throughput_ops_s": 633958.6015719158,
+        "iterations_ops_s": [
+          617689.5090250478, 649548.5363109286, 634637.7593797712
+        ],
+        "allocation_bytes_op": 1128.0092592375615,
+        "artifact_sha256": {
+          "fory_json": "0b175646200572bd67817e5ff6350ca9b41ecf8d9c5fedb46ef90677fe335014",
+          "fory_scala": "69bce71e74891f86f0a2b1a607b2a86743955e5378386dbc3ab72e64c30f62cd",
+          "fory_core": "1d743236d0529f1e0c082bba9abc36bf502167add84905441ad917f7143b5b9a",
+          "benchmark": "606d03f7f6fb4bbf217b3e2d30ae82054d702aa937148328b1987191bd63e3b4"
+        }
+      }
+    },
+    {
+      "pair": 2,
+      "measurement_iterations": 3,
+      "baseline": {
+        "throughput_ops_s": 540338.0918875013,
+        "iterations_ops_s": [
+          536275.7264876695, 526926.7419483826, 557811.8072264518
+        ],
+        "allocation_bytes_op": 1656.0108723773349,
+        "artifact_sha256": {
+          "fory_json": "0b175646200572bd67817e5ff6350ca9b41ecf8d9c5fedb46ef90677fe335014",
+          "fory_scala": "69bce71e74891f86f0a2b1a607b2a86743955e5378386dbc3ab72e64c30f62cd",
+          "fory_core": "1d743236d0529f1e0c082bba9abc36bf502167add84905441ad917f7143b5b9a",
+          "benchmark": "606d03f7f6fb4bbf217b3e2d30ae82054d702aa937148328b1987191bd63e3b4"
+        }
+      },
+      "fory": {
+        "throughput_ops_s": 720624.0504748118,
+        "iterations_ops_s": [
+          725308.188246871, 717928.044117385, 718635.9190601795
+        ],
+        "allocation_bytes_op": 528.0081332921432,
+        "artifact_sha256": {
+          "fory_json": "3dc8c8cce674855b39faca03156bf57048bb28d1503513cd5d5a85c6935ea696",
+          "fory_scala": "69bce71e74891f86f0a2b1a607b2a86743955e5378386dbc3ab72e64c30f62cd",
+          "fory_core": "1d743236d0529f1e0c082bba9abc36bf502167add84905441ad917f7143b5b9a",
+          "benchmark": "606d03f7f6fb4bbf217b3e2d30ae82054d702aa937148328b1987191bd63e3b4"
+        }
+      },
+      "jsoniter_scala": {
+        "throughput_ops_s": 637647.9827981751,
+        "iterations_ops_s": [
+          633147.7919315896, 637860.5415264402, 641935.6149364957
+        ],
+        "allocation_bytes_op": 1128.0092273109979,
+        "artifact_sha256": {
+          "fory_json": "0b175646200572bd67817e5ff6350ca9b41ecf8d9c5fedb46ef90677fe335014",
+          "fory_scala": "69bce71e74891f86f0a2b1a607b2a86743955e5378386dbc3ab72e64c30f62cd",
+          "fory_core": "1d743236d0529f1e0c082bba9abc36bf502167add84905441ad917f7143b5b9a",
+          "benchmark": "606d03f7f6fb4bbf217b3e2d30ae82054d702aa937148328b1987191bd63e3b4"
+        }
+      }
+    }
+  ],
+  "integer_read_control": [
+    {
+      "pair": 1,
+      "baseline": {
+        "throughput_ops_s": 491577.15519360424,
+        "iterations_ops_s": [
+          493226.1424904787, 487426.7022866886, 491108.85466615774,
+          489525.76150114706, 496598.3150235495
+        ],
+        "allocation_bytes_op": 4176.011942924268,
+        "artifact_sha256": {
+          "fory_json": "0b175646200572bd67817e5ff6350ca9b41ecf8d9c5fedb46ef90677fe335014",
+          "fory_scala": "69bce71e74891f86f0a2b1a607b2a86743955e5378386dbc3ab72e64c30f62cd",
+          "fory_core": "1d743236d0529f1e0c082bba9abc36bf502167add84905441ad917f7143b5b9a",
+          "benchmark": "606d03f7f6fb4bbf217b3e2d30ae82054d702aa937148328b1987191bd63e3b4"
+        }
+      },
+      "fory": {
+        "throughput_ops_s": 498496.5149580714,
+        "iterations_ops_s": [
+          500744.575380113, 500638.4030852225, 493489.2902459867,
+          498268.2111324434, 499342.09494659153
+        ],
+        "allocation_bytes_op": 4176.011777540917,
+        "artifact_sha256": {
+          "fory_json": "a60312c784247481c49a6a7a2bf128979fdba070c305a0e1f95966673237883b",
+          "fory_scala": "69bce71e74891f86f0a2b1a607b2a86743955e5378386dbc3ab72e64c30f62cd",
+          "fory_core": "1d743236d0529f1e0c082bba9abc36bf502167add84905441ad917f7143b5b9a",
+          "benchmark": "606d03f7f6fb4bbf217b3e2d30ae82054d702aa937148328b1987191bd63e3b4"
+        }
+      }
+    },
+    {
+      "pair": 2,
+      "baseline": {
+        "throughput_ops_s": 487399.4866039612,
+        "iterations_ops_s": [
+          492882.917488924, 491449.6032085084, 492271.10849723214,
+          483111.02830760553, 477282.77551753627
+        ],
+        "allocation_bytes_op": 4176.012066509457,
+        "artifact_sha256": {
+          "fory_json": "0b175646200572bd67817e5ff6350ca9b41ecf8d9c5fedb46ef90677fe335014",
+          "fory_scala": "69bce71e74891f86f0a2b1a607b2a86743955e5378386dbc3ab72e64c30f62cd",
+          "fory_core": "1d743236d0529f1e0c082bba9abc36bf502167add84905441ad917f7143b5b9a",
+          "benchmark": "606d03f7f6fb4bbf217b3e2d30ae82054d702aa937148328b1987191bd63e3b4"
+        }
+      },
+      "fory": {
+        "throughput_ops_s": 492338.3696801558,
+        "iterations_ops_s": [
+          482311.38343038387, 493766.54553907254, 493700.9130787423,
+          495583.89678214624, 496329.1095704343
+        ],
+        "allocation_bytes_op": 4176.01193743859,
+        "artifact_sha256": {
+          "fory_json": "a60312c784247481c49a6a7a2bf128979fdba070c305a0e1f95966673237883b",
+          "fory_scala": "69bce71e74891f86f0a2b1a607b2a86743955e5378386dbc3ab72e64c30f62cd",
+          "fory_core": "1d743236d0529f1e0c082bba9abc36bf502167add84905441ad917f7143b5b9a",
+          "benchmark": "606d03f7f6fb4bbf217b3e2d30ae82054d702aa937148328b1987191bd63e3b4"
+        }
+      }
+    }
+  ]
+}
diff --git a/docs/benchmarks/json/byte-array-formats.md b/docs/benchmarks/json/byte-array-formats.md
new file mode 100644
index 000000000..51b915f1c
--- /dev/null
+++ b/docs/benchmarks/json/byte-array-formats.md
@@ -0,0 +1,105 @@
+# JVM JSON byte-array format measurements
+
+This comparison covers numeric byte arrays and Base16 strings on the unchanged
+jsoniter-scala `ArrayOfBytesReading`, `ArrayOfBytesWriting`, `Base16Reading`, and
+`Base16Writing` workloads at `size=512`. Higher throughput is better.
+
+All 12 final adjacent comparison pairs exceed 10%. Numeric-array reading has a
+narrow margin: its smallest measured improvement is 10.05%. These are local
+steady-state measurements, not a guarantee for other sizes, data distributions,
+machines, or JVMs.
+
+## Results
+
+| Workload                   | jsoniter-scala median, ops/s | Fory median, ops/s | Median paired improvement |         Paired range |
+| -------------------------- | ---------------------------: | -----------------: | ------------------------: | -------------------: |
+| Numeric byte-array reading |                      646,911 |            713,856 |                   +10.18% |   +10.05% to +11.55% |
+| Numeric byte-array writing |                    1,539,685 |          3,350,581 |                  +117.93% | +116.07% to +118.36% |
+| Base16 reading             |                    2,914,929 |          3,585,325 |                   +22.18% |   +15.26% to +24.77% |
+| Base16 writing             |                    8,444,372 |          9,639,286 |                   +15.94% |   +13.78% to +16.00% |
+
+Each pair compares the means of five measurement iterations. The improvement
+column is the median of those paired ratios, not the ratio of independent medians.
+All final samples are retained. Earlier, shorter numeric-reading comparisons
+measured +8.59% and +13.01%; they are also included in the evidence and illustrate
+why the final narrow margin should not be generalized.
+
+Allocation per operation is approximately 528 B for Fory numeric-array reading
+and 1,128 B for jsoniter-scala. Both libraries allocate about 1,888 B for numeric
+array output, 528 B for Base16 input, and 1,048 B for Base16 output. Both use APIs
+that return independent arrays; neither uses the preallocated-output benchmark.
+Startup and retained lookup-table memory are outside these steady-state allocation
+figures. Fory's Base16 output table occupies 256 KiB after its first use.
+
+The affected integer-reading path was also compared with the pre-optimization
+Fory revision in two adjacent pairs. Changes were +1.41% and +1.01%, within the
+range normally treated as noise; no regression was observed in those runs.
+
+[Iteration samples, allocations, paired results, and artifact hashes](byte-array-formats-results.json)
+are available as JSON.
+
+## Workloads and configuration
+
+The input bytes are the benchmark's original sequence
+`(1 to size).map(_.toByte).toArray`. Numeric arrays keep the original signed decimal
+JSON representation. Base16 keeps the original lowercase hexadecimal string.
+Models, inputs, expected outputs, and jsoniter-scala implementations are unchanged.
+Only the missing Fory adapters and their existing correctness specs are enabled.
+
+Fory uses separate instances configured through its public builder:
+
+```java
+import org.apache.fory.json.ForyJson;
+import org.apache.fory.json.annotation.JsonByteArray;
+
+ForyJson arrays = ForyJson.builder().byteArrayFormat(JsonByteArray.Format.ARRAY).build();
+ForyJson hex = ForyJson.builder().byteArrayFormat(JsonByteArray.Format.BASE16).build();
+```
+
+Each reading adapter calls `fromJson(jsonBytes, classOf[Array[Byte]])`; each writing
+adapter calls `toJsonBytes(obj)`. These configurations also apply to nested byte
+arrays; they are not benchmark-specific codecs.
+
+## Environment and revisions
+
+- Date: September 21, 2026.
+- Apple M4 Pro, 12 logical CPUs, 48 GiB RAM; macOS 15.7.2 arm64.
+- OpenJDK 25.0.3; JMH 1.37.
+- Fory: `13739263ebf31af5fc962184aaccf2833f8ffc9a`, version `1.8.0-SNAPSHOT`.
+- Fory pre-optimization baseline: `e1d2f9d67800901392755b1133968d448ab076b8`.
+- jsoniter-scala: `2.40.2-SNAPSHOT`, checkout
+  `71b2ba16f1d5261e2be4e98d5dfe4be452d0e136`; its Fory adapter additions are based on
+  `84cb277f2c2a16b4018bc61fdeec3eed62252a1e`.
+- Benchmark Scala: 3.9.0; Fory JSON Scala: 3.3.8.
+- One thread and one fork per run; three 1-second warmups and five 1-second
+  measurements; `-Xms1g -Xmx1g -XX:+UseParallelGC`; JMH GC profiler.
+- Three adjacent pairs per workload. Order is jsoniter/Fory, Fory/jsoniter,
+  jsoniter/Fory. Runs are serial; normal desktop background activity remains.
+
+The runtime, core, Scala, and benchmark jars are frozen during measurement. Only
+the Fory JSON runtime jar changes in baseline/current controls.
+
+## Reproduction
+
+Install Fory core, JSON, and JSON Scala artifacts before building the benchmark
+assembly. Enable Maven Local when using locally published snapshots:
+
+```bash
+sbt --java-home "$JAVA_HOME" \
+  'set ThisBuild / resolvers += Resolver.mavenLocal' \
+  '++3.9.0!' 'jsoniter-scala-benchmarkJVM/assembly'
+```
+
+Run each method as a separate process, alternating the library order between
+pairs. Use the same frozen classpath for both methods:
+
+```bash
+java -cp "$FORY_JSON_JAR:$FORY_SCALA_JAR:$FORY_CORE_JAR:$BENCHMARK_JAR" \
+  org.openjdk.jmh.Main ".*\.${CASE}\.${METHOD}$" \
+  -t 1 -f 1 -wi 3 -i 5 -w 1s -r 1s -p size=512 \
+  -jvmArgs '-Xms1g -Xmx1g -XX:+UseParallelGC' -foe true -prof gc \
+  -rf json -rff "$RESULT_FILE"
+```
+
+`CASE` is one of the four workload names above; `METHOD` is `fory` or
+`jsoniterScala`. No GraalVM or native-image performance is measured here.
diff --git a/docs/json/annotations.md b/docs/json/annotations.md
index e6975f463..be2adefc8 100644
--- a/docs/json/annotations.md
+++ b/docs/json/annotations.md
@@ -177,7 +177,7 @@ With an empty `items` list, this object writes `{}`. Explicit property inclusion
 class and builder defaults. Java empty-value checks run directly, even with a custom codec.
 Other types use the selected codec's `isEmpty(writer, value)`, which defaults to `false`.
 A custom codec must override it to define empty values; writing `""` or `{}` alone does not make
-an ordinary object empty. An empty `byte[]` is empty with either Base64 or numeric-array
+an ordinary object empty. An empty `byte[]` is empty with Base64, Base16, or numeric-array
 representation. See [Custom codecs](custom-codecs.md#custom-empty-values).
 
 Filtering is shallow: `0`, `false`, a list containing null, a list containing an empty list, and a
@@ -477,8 +477,8 @@ as a trusted raw root value. That combination is serialization-only: the ordinar
 
 ## `JsonByteArray`
 
-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:
+`JsonByteArray` selects `BASE64`, `BASE16`, or `ARRAY` for one exact `byte[]` field or getter, in
+both reading and writing. It overrides the builder's `byteArrayFormat`, whose default is `BASE64`:
 
 ```java
 import org.apache.fory.json.annotation.JsonByteArray;
@@ -489,10 +489,16 @@ public final class Attachment {
 
   @JsonByteArray(JsonByteArray.Format.BASE64)
   public byte[] content;
+
+  @JsonByteArray(JsonByteArray.Format.BASE16)
+  public byte[] hex;
 }
 ```
 
-For bytes `{1, -2, 3}`, `numbers` is written as `[1,-2,3]` and `content` as `"Af4D"`.
+For bytes `{1, -2, 3}`, `numbers` is written as `[1,-2,3]`, `content` as `"Af4D"`, and `hex` as
+`"01fe03"`. `BASE16` writes lowercase hexadecimal digits without a prefix or separators; it reads
+uppercase or lowercase digits, including JSON string escapes, and rejects odd-length or invalid
+hexadecimal strings.
 `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
@@ -503,7 +509,7 @@ property, not to container elements or map values. Mixin declarations can select
 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.
 
-Base64 values are binary leaves excluded from the graph-memory budget. Numeric arrays count their
+Base64 and Base16 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`
diff --git a/docs/json/graalvm.md b/docs/json/graalvm.md
index 22d48b3f9..7a67321e4 100644
--- a/docs/json/graalvm.md
+++ b/docs/json/graalvm.md
@@ -209,8 +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 `JsonByteArray` fields and getters select Base64 strings or numeric
-byte arrays 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,
+Base16 strings, or numeric byte arrays as on the JVM. The builder's `byteArrayFormat` controls the
+default representation; include that setting in a provider when using generated object codecs.
+`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 selected byte-array
 codec constructor.
diff --git a/docs/json/kotlin.md b/docs/json/kotlin.md
index cc7a35576..63a70d24e 100644
--- a/docs/json/kotlin.md
+++ b/docs/json/kotlin.md
@@ -93,6 +93,24 @@ collection and map values, nullable values, Kotlin value classes backed by them,
 the Java Long-like wrappers supported by the core JSON runtime. Readers accept both quoted and
 unquoted integer tokens.
 
+`ByteArray` uses Base64 strings by default. The standard builder's `byteArrayFormat` also controls
+root arrays and arrays nested in properties, collections, and maps:
+
+```kotlin
+import org.apache.fory.json.annotation.JsonByteArray
+import org.apache.fory.json.kotlin.ForyJsonKotlin
+import org.apache.fory.json.kotlin.jsonTypeRef
+
+val hexJson = ForyJsonKotlin.builder().byteArrayFormat(JsonByteArray.Format.BASE16).build()
+val bytesType = jsonTypeRef<ByteArray>()
+val text = hexJson.toJson(byteArrayOf(1, -2, 3), bytesType) // "\"01fe03\""
+val bytes = hexJson.fromJson(text, bytesType)
+```
+
+`@field:JsonByteArray` or `@get:JsonByteArray`, including a Mixin declaration, overrides the default
+for that property. `UByteArray` retains its unsigned numeric-array representation. See
+[byte-array formats](object-mapping.md#builder-configuration) for accepted input and null behavior.
+
 `jsonTypeRef<T>()` is a type token, not a codec lookup. Construct it once and reuse it. A Java
 `Class` or ordinary Java `TypeRef` cannot express distinctions such as `List<Account?>`, `UInt`, or
 a logical value class lowered to a primitive carrier.
diff --git a/docs/json/object-mapping.md b/docs/json/object-mapping.md
index 63382d318..4cd8a405d 100644
--- a/docs/json/object-mapping.md
+++ b/docs/json/object-mapping.md
@@ -229,20 +229,42 @@ for supported sources, construction/evaluation effects, and errors. An explicit
 overrides `JsonInclude` on the class, which overrides the builder default. See
 [Property inclusion](annotations.md#jsonproperty) for the empty-value definitions and boundaries.
 
-| Builder method                         | Default                                   | User-visible effect                                        |
-| -------------------------------------- | ----------------------------------------- | ---------------------------------------------------------- |
-| `defaultPropertyInclusion(Include)`    | `NON_NULL`                                | Default inclusion of object properties                     |
-| `writeNullFields(boolean)`             | `false`                                   | Select `ALWAYS` when true or `NON_NULL` when false         |
-| `writeLongAsString(boolean)`           | `false`                                   | Write built-in 64-bit integer values as decimal strings    |
-| `withCodegen(boolean)`                 | `true`                                    | Enable generated object codecs                             |
-| `withAsyncCompilation(boolean)`        | `true`                                    | Compile generated codecs asynchronously                    |
-| `withFieldMode(boolean)`               | `false`                                   | When true, discover fields without getters/setters         |
-| `withPropertyNamingStrategy(strategy)` | `LOWER_CAMEL_CASE`                        | Name properties without an explicit `JsonProperty` name    |
-| `withMaxCachedFieldNames(int)`         | `DEFAULT_MAX_CACHED_FIELD_NAMES` (`8192`) | Field-name cache entries per reader; zero disables caching |
-| `withConcurrencyLevel(int)`            | `max(1, 2 * processors)`                  | Maximum concurrent root operations                         |
-| `withBufferSizeLimitBytes(int)`        | 2 MiB                                     | Maximum retained output/string-decoding buffer capacity    |
-| `registerCodec(type, codec)`           | None                                      | Replace an eligible exact class's complete JSON codec      |
-| `registerMixin(mixinType)`             | None                                      | Apply one annotation Mixin to its exact declared target    |
+| Builder method                          | Default                                   | User-visible effect                                        |
+| --------------------------------------- | ----------------------------------------- | ---------------------------------------------------------- |
+| `defaultPropertyInclusion(Include)`     | `NON_NULL`                                | Default inclusion of object properties                     |
+| `writeNullFields(boolean)`              | `false`                                   | Select `ALWAYS` when true or `NON_NULL` when false         |
+| `writeLongAsString(boolean)`            | `false`                                   | Write built-in 64-bit integer values as decimal strings    |
+| `byteArrayFormat(JsonByteArray.Format)` | `BASE64`                                  | Default byte-array representation for reading and writing  |
+| `withCodegen(boolean)`                  | `true`                                    | Enable generated object codecs                             |
+| `withAsyncCompilation(boolean)`         | `true`                                    | Compile generated codecs asynchronously                    |
+| `withFieldMode(boolean)`                | `false`                                   | When true, discover fields without getters/setters         |
+| `withPropertyNamingStrategy(strategy)`  | `LOWER_CAMEL_CASE`                        | Name properties without an explicit `JsonProperty` name    |
+| `withMaxCachedFieldNames(int)`          | `DEFAULT_MAX_CACHED_FIELD_NAMES` (`8192`) | Field-name cache entries per reader; zero disables caching |
+| `withConcurrencyLevel(int)`             | `max(1, 2 * processors)`                  | Maximum concurrent root operations                         |
+| `withBufferSizeLimitBytes(int)`         | 2 MiB                                     | Maximum retained output/string-decoding buffer capacity    |
+| `registerCodec(type, codec)`            | None                                      | Replace an eligible exact class's complete JSON codec      |
+| `registerMixin(mixinType)`              | None                                      | Apply one annotation Mixin to its exact declared target    |
+
+Use `byteArrayFormat` to choose one representation for ordinary `byte[]` roots, unannotated
+properties, and container values, including nested arrays, collections, maps, and optionals:
+
+```java
+import org.apache.fory.json.ForyJson;
+import org.apache.fory.json.annotation.JsonByteArray;
+
+ForyJson hexJson = ForyJson.builder().byteArrayFormat(JsonByteArray.Format.BASE16).build();
+String text = hexJson.toJson(new byte[] {1, -2, 3}); // "\"01fe03\""
+byte[] bytes = hexJson.fromJson(text, byte[].class);
+```
+
+`BASE64` is the default and writes padded standard Base64 strings. `BASE16` writes lowercase
+hexadecimal strings without prefixes or separators; reading accepts either case and JSON string
+escapes. Odd-length strings and non-hexadecimal characters are rejected. `ARRAY` uses signed
+numbers in `[-128, 127]`. Empty arrays become `""` for either string format or `[]` for `ARRAY`.
+All three formats accept JSON `null`. The reader uses the configured representation and does not
+guess another format. A [property's `JsonByteArray` annotation](annotations.md#jsonbytearray),
+including one supplied by a Mixin, overrides the global default. Custom occurrence codecs retain
+their complete representation; Kotlin unsigned semantic arrays remain numeric arrays.
 
 Enable `writeLongAsString(true)` when 64-bit integer values must pass through JavaScript without
 `Number` precision loss. The setting writes built-in `long`/`Long`, `AtomicLong`,
diff --git a/docs/json/scala.md b/docs/json/scala.md
index 2054d0ec9..2f31e3efb 100644
--- a/docs/json/scala.md
+++ b/docs/json/scala.md
@@ -47,6 +47,25 @@ wrappers as quoted decimal strings. Readers accept both quoted and unquoted inte
 Use `ScalaTypeRef` when a parameterized declaration contains `Long` because normal JVM signatures
 can erase Scala value-type arguments to `Object`.
 
+## Byte-array formats
+
+Scala `Array[Byte]` uses Base64 strings by default. Use the standard builder's
+`byteArrayFormat` to select numeric arrays or hexadecimal strings, including arrays inside
+`Option`, Scala collections, and maps:
+
+```scala
+import org.apache.fory.json.annotation.JsonByteArray
+import org.apache.fory.json.scala.{ForyJsonScala, ScalaTypeRef}
+
+val hexJson = ForyJsonScala.builder().byteArrayFormat(JsonByteArray.Format.BASE16).build()
+val bytesType = ScalaTypeRef[Array[Byte]]
+val text = hexJson.toJson(Array[Byte](1, -2, 3), bytesType) // "\"01fe03\""
+val bytes = hexJson.fromJson(text, bytesType)
+```
+
+`JsonByteArray` on a field or getter, including a Mixin, overrides the default for that property.
+See [byte-array formats](object-mapping.md#builder-configuration) for the read/write contract.
+
 ## Case classes and annotations
 
 Case classes are decoded by calling their full primary constructor. Fory invokes Scala's generated
diff --git a/docs/json/security.md b/docs/json/security.md
index deecb49d4..3de92c2f6 100644
--- a/docs/json/security.md
+++ b/docs/json/security.md
@@ -101,7 +101,7 @@ is also charged once; a cached empty Optional singleton is not a new graph owner
 Dedicated leaf codecs are excluded from graph accounting: null, strings,
 characters, booleans, numeric values including arbitrary-precision numbers,
 enums, temporal and other scalar values, and binary values. A `byte[]` handled
-by a binary or Base64 codec remains a binary leaf; the same Java carrier read
+by a Base64 or Base16 codec remains a binary leaf; the same Java carrier read
 from a JSON numeric array is a primitive-array owner. Byte-availability and
 grammar checks still apply independently of graph accounting.
 
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 1cbd7fad3..c596eb207 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
@@ -104,6 +104,7 @@ public final class ForyJsonExample {
     testInclusion();
     testCodecs();
     testValueAnnotations();
+    testByteArrayFormats();
     testSubtypes();
     testSubtypeMixin();
     testContainerRoots();
@@ -594,6 +595,53 @@ public final class ForyJsonExample {
             json.fromJson("{\"value\":\"AQID\"}", Base64Bytes.class).value, new byte[] {1, 2, 3}));
   }
 
+  private static void testByteArrayFormats() {
+    byte[] bytes = {1, -2, 3};
+    JsonByteArray.Format[] formats = JsonByteArray.Format.values();
+    String[] encodings = {"\"Af4D\"", "[1,-2,3]", "\"01fe03\""};
+    for (int i = 0; i < formats.length; i++) {
+      ForyJson json = ForyJson.builder().byteArrayFormat(formats[i]).build();
+      String encoded = encodings[i];
+      Preconditions.checkArgument(json.toJson(bytes).equals(encoded));
+      Preconditions.checkArgument(
+          new String(json.toJsonBytes(bytes), StandardCharsets.UTF_8).equals(encoded));
+      Preconditions.checkArgument(Arrays.equals(json.fromJson(encoded, byte[].class), bytes));
+      Preconditions.checkArgument(
+          Arrays.equals(json.fromJson(json.toPrettyJsonBytes(bytes), byte[].class), bytes));
+      if (GraalvmSupport.isGraalRuntime()) {
+        CodegenProbeCodec.expect(CodegenProbeModel.class, true, true);
+        CodegenProbeModel model = new CodegenProbeModel();
+        model.bytes = bytes;
+        for (String probe : new String[] {"probe", "汉"}) {
+          model.probe = new CodegenProbeValue(probe);
+          String text = json.toJson(model);
+          Preconditions.checkArgument(text.contains("\"bytes\":" + encoded));
+          Preconditions.checkArgument(
+              new String(json.toJsonBytes(model), StandardCharsets.UTF_8).equals(text));
+          Preconditions.checkArgument(
+              Arrays.equals(json.fromJson(text, CodegenProbeModel.class).bytes, bytes));
+          Preconditions.checkArgument(
+              Arrays.equals(
+                  json.fromJson(json.toPrettyJsonBytes(model), CodegenProbeModel.class).bytes,
+                  bytes));
+        }
+      }
+    }
+    HexBytes value = new HexBytes();
+    value.value = bytes;
+    for (boolean codegen : new boolean[] {false, true}) {
+      ForyJson json = ForyJson.builder().withCodegen(codegen).withAsyncCompilation(false).build();
+      String encoded = "{\"value\":\"01fe03\"}";
+      Preconditions.checkArgument(json.toJson(value).equals(encoded));
+      Preconditions.checkArgument(
+          new String(json.toJsonBytes(value), StandardCharsets.UTF_8).equals(encoded));
+      Preconditions.checkArgument(
+          Arrays.equals(json.fromJson(encoded, HexBytes.class).value, bytes));
+      Preconditions.checkArgument(
+          Arrays.equals(json.fromJson(json.toPrettyJsonBytes(value), HexBytes.class).value, bytes));
+    }
+  }
+
   private static void testSubtypes() {
     ForyJson json = ForyJson.builder().build();
     Shape value = new Circle(9);
@@ -879,6 +927,14 @@ public final class ForyJsonExample {
     default ForyJson nonEmptyConfiguration() {
       return ForyJson.builder().defaultPropertyInclusion(JsonProperty.Include.NON_EMPTY).build();
     }
+
+    default ForyJson base16Configuration() {
+      return ForyJson.builder().byteArrayFormat(JsonByteArray.Format.BASE16).build();
+    }
+
+    default ForyJson byteArrayConfiguration() {
+      return ForyJson.builder().byteArrayFormat(JsonByteArray.Format.ARRAY).build();
+    }
   }
 
   @JsonType
@@ -893,6 +949,7 @@ public final class ForyJsonExample {
   @JsonType
   public static final class CodegenProbeModel {
     public int id;
+    public byte[] bytes;
 
     @JsonCodec(CodegenProbeCodec.class)
     public CodegenProbeValue probe;
@@ -1472,6 +1529,12 @@ public final class ForyJsonExample {
     public byte[] value;
   }
 
+  @JsonType
+  public static final class HexBytes {
+    @JsonByteArray(JsonByteArray.Format.BASE16)
+    public byte[] value;
+  }
+
   @JsonType
   public static final class ConfigValue {
     public String camelName;
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 754d82f3f..f42d79e7f 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
@@ -32,6 +32,7 @@ public object PlatformCorpusChecks {
     check(text.contains("\"display_label\":\"mixin\""))
     check(text.contains("\"numbers\":[1,-2,3]"))
     check(text.contains("\"binary\":\"Af4D\""))
+    check(text.contains("\"hex\":\"01fe03\""))
     check(text.contains("\"defaultBytes\":\"Af4D\""))
     verifyRoot(json.fromJson(text, type))
     verifyRoot(json.fromJson(json.toJsonBytes(decoded, type), type))
@@ -48,6 +49,7 @@ public object PlatformCorpusChecks {
     check(actual.box == expected.box)
     check(actual.numbers.contentEquals(expected.numbers))
     check(actual.binary.contentEquals(expected.binary))
+    check(actual.hex.contentEquals(expected.hex))
     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 a3c81ac68..1467665cc 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
@@ -125,6 +125,8 @@ public data class PlatformRoot(
   public val numbers: ByteArray = byteArrayOf(1, -2, 3),
   @get:JsonByteArray(JsonByteArray.Format.BASE64)
   public val binary: ByteArray = byteArrayOf(1, -2, 3),
+  @field:JsonByteArray(JsonByteArray.Format.BASE16)
+  public val hex: ByteArray = byteArrayOf(1, -2, 3),
   public val defaultBytes: ByteArray = byteArrayOf(1, -2, 3),
 )
 
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 dd887edcc..2b3760f97 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
@@ -89,6 +89,7 @@ public class KspRetentionResourceTest {
       root,
     )
     assertConstructor(root, "org.apache.fory.json.codec.Base64ByteArrayCodec")
+    assertConstructor(root, "org.apache.fory.json.codec.Base16ByteArrayCodec")
     assertConstructor(root, "org.apache.fory.json.codec.ArrayCodec\$SignedByteArrayCodec")
     assertConstructor(
       rules("PlatformDirectOverride"),
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 083cb22cd..197916aea 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
@@ -575,7 +575,9 @@ final class JsonTypeProcessor {
       model.codecTypes.add(
           format.getSimpleName().contentEquals("ARRAY")
               ? JSON_PACKAGE + ".codec.ArrayCodec$SignedByteArrayCodec"
-              : BASE64_CODEC);
+              : format.getSimpleName().contentEquals("BASE16")
+                  ? JSON_PACKAGE + ".codec.Base16ByteArrayCodec"
+                  : BASE64_CODEC);
     }
   }
 
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 029396add..323948bb1 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
@@ -51,6 +51,7 @@ import javax.tools.StandardJavaFileManager;
 import javax.tools.ToolProvider;
 import org.apache.fory.json.ForyJson;
 import org.apache.fory.json.ForyJsonException;
+import org.apache.fory.json.annotation.JsonByteArray;
 import org.apache.fory.json.codec.GeneratedJsonCodec;
 import org.apache.fory.json.meta.JsonAnySetterAccessor;
 import org.apache.fory.json.meta.JsonFieldAccessor;
@@ -967,6 +968,53 @@ public class JsonTypeProcessorTest {
         new Object[] {5, "mutable", extra});
   }
 
+  @Test
+  public void byteArrayMixinPipeline() throws Exception {
+    CompilationResult result =
+        compile(
+            "test.BinaryTarget",
+            "package test;\n"
+                + "import org.apache.fory.json.annotation.*;\n"
+                + "public final class BinaryTarget { public byte[] value; public byte[] other; }\n"
+                + "@JsonMixin(target = BinaryTarget.class) abstract class BinaryMixin {\n"
+                + "  @JsonByteArray(JsonByteArray.Format.BASE16) byte[] value;\n"
+                + "}\n");
+    assertTrue(result.success, result.diagnostics());
+    String companion = "test.BinaryMixin_ForyJsonMixin_test_x2e_BinaryTarget_ForyJsonCodec";
+    ClassLoader loader = result.classLoader();
+    GeneratedJsonCodec<?> generated = generatedCodec(loader, companion);
+    Class<?> target = loader.loadClass("test.BinaryTarget");
+    Class<?> mixin = loader.loadClass("test.BinaryMixin");
+    byte[] bytes = {1, -2};
+    Object value = target.getConstructor().newInstance();
+    target.getField("value").set(value, bytes);
+    target.getField("other").set(value, bytes);
+    assertEquals(generated.type(), target);
+    assertEquals(fieldAccessor(generated.fieldAccessors(), "value").getObject(value), bytes);
+    String rules = result.generatedResource(MIXIN_RULE_PREFIX + "test.BinaryMixin.pro");
+    assertTrue(
+        rules.contains(
+            "class org.apache.fory.json.codec.Base16ByteArrayCodec { public <init>(); }"),
+        rules);
+    for (boolean codegen : new boolean[] {false, true}) {
+      ForyJson json =
+          ForyJson.builder()
+              .withCodegen(codegen)
+              .withAsyncCompilation(false)
+              .withClassLoader(loader)
+              .registerMixin(mixin)
+              .byteArrayFormat(JsonByteArray.Format.ARRAY)
+              .build();
+      String text = json.toJson(value);
+      assertTrue(text.contains("\"value\":\"01fe\""), text);
+      assertTrue(text.contains("\"other\":[1,-2]"), text);
+      assertEquals(new String(json.toJsonBytes(value), StandardCharsets.UTF_8), text);
+      Object decoded = json.fromJson(json.toPrettyJsonBytes(value), target);
+      assertEquals(target.getField("value").get(decoded), bytes);
+      assertEquals(target.getField("other").get(decoded), bytes);
+    }
+  }
+
   @Test
   public void encodedRecordPipeline() throws Exception {
     assumeJava16Source();
@@ -2015,6 +2063,7 @@ public class JsonTypeProcessorTest {
             + "  @JsonRawValue public String body;\n"
             + "  @JsonByteArray(JsonByteArray.Format.BASE64) public byte[] bytes;\n"
             + "  @JsonByteArray(JsonByteArray.Format.ARRAY) public byte[] numbers;\n"
+            + "  @JsonByteArray(JsonByteArray.Format.BASE16) public byte[] hex;\n"
             + "  private String other;\n"
             + "  @JsonRawValue public String getOther() { return other; }\n"
             + "  public void setOther(String other) { this.other = other; }\n"
@@ -2032,6 +2081,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.Base16ByteArrayCodec { public <init>(); }"),
+        rawRules);
     assertTrue(
         rawRules.contains(
             "class org.apache.fory.json.codec.ArrayCodec$SignedByteArrayCodec { public <init>(); }"),
diff --git a/java/fory-json/src/main/java/org/apache/fory/json/ForyJsonBuilder.java b/java/fory-json/src/main/java/org/apache/fory/json/ForyJsonBuilder.java
index 2a0a94a9d..4c6cee91a 100644
--- a/java/fory-json/src/main/java/org/apache/fory/json/ForyJsonBuilder.java
+++ b/java/fory-json/src/main/java/org/apache/fory/json/ForyJsonBuilder.java
@@ -24,6 +24,7 @@ import java.util.IdentityHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
+import org.apache.fory.json.annotation.JsonByteArray;
 import org.apache.fory.json.annotation.JsonMixin;
 import org.apache.fory.json.annotation.JsonProperty.Include;
 import org.apache.fory.json.codec.JsonValueCodec;
@@ -52,6 +53,7 @@ import org.apache.fory.platform.GraalvmSupport;
 public final class ForyJsonBuilder {
   private Include defaultPropertyInclusion = Include.NON_NULL;
   private boolean writeLongAsString;
+  private JsonByteArray.Format byteArrayFormat = JsonByteArray.Format.BASE64;
   private boolean codegenEnabled = true;
   private boolean asyncCompilationEnabled = true;
   private boolean propertyDiscoveryEnabled = true;
@@ -128,6 +130,22 @@ public final class ForyJsonBuilder {
     return this;
   }
 
+  /**
+   * Sets the default representation for {@code byte[]} roots, properties, and container values in
+   * both reading and writing. The default is {@link JsonByteArray.Format#BASE64}.
+   *
+   * <p>{@link JsonByteArray} on a property or its Mixin overrides this setting. Custom occurrence
+   * codecs and language-module unsigned arrays retain their own representation. Base16 writes
+   * lowercase hexadecimal digits without a prefix and accepts either case when reading. Numeric
+   * arrays use signed values in {@code [-128, 127]}. Formats are not detected automatically.
+   *
+   * @throws NullPointerException if format is null
+   */
+  public ForyJsonBuilder byteArrayFormat(JsonByteArray.Format format) {
+    byteArrayFormat = Objects.requireNonNull(format, "format");
+    return this;
+  }
+
   /**
    * Enables generated object codecs for supported classes. Enabled by default and automatically
    * disabled on Android. A GraalVM Native Image includes generated codecs for the default
@@ -346,6 +364,7 @@ public final class ForyJsonBuilder {
     return new JsonConfig(
         defaultPropertyInclusion,
         writeLongAsString,
+        byteArrayFormat,
         effectiveCodegen,
         effectiveAsyncCompilation,
         propertyDiscoveryEnabled,
diff --git a/java/fory-json/src/main/java/org/apache/fory/json/JsonConfig.java b/java/fory-json/src/main/java/org/apache/fory/json/JsonConfig.java
index 3adb638f4..d271e97e4 100644
--- a/java/fory-json/src/main/java/org/apache/fory/json/JsonConfig.java
+++ b/java/fory-json/src/main/java/org/apache/fory/json/JsonConfig.java
@@ -25,6 +25,7 @@ import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 import org.apache.fory.annotation.Internal;
+import org.apache.fory.json.annotation.JsonByteArray;
 import org.apache.fory.json.annotation.JsonProperty.Include;
 import org.apache.fory.json.resolver.CodecRegistry;
 
@@ -39,6 +40,7 @@ public final class JsonConfig {
 
   private final Include defaultPropertyInclusion;
   private final boolean writeLongAsString;
+  private final JsonByteArray.Format byteArrayFormat;
   private final boolean codegenEnabled;
   private final boolean asyncCompilationEnabled;
   private final boolean propertyDiscoveryEnabled;
@@ -59,6 +61,7 @@ public final class JsonConfig {
   JsonConfig(
       Include defaultPropertyInclusion,
       boolean writeLongAsString,
+      JsonByteArray.Format byteArrayFormat,
       boolean codegenEnabled,
       boolean asyncCompilationEnabled,
       boolean propertyDiscoveryEnabled,
@@ -76,6 +79,7 @@ public final class JsonConfig {
       JsonTypeChecker typeChecker) {
     this.defaultPropertyInclusion = defaultPropertyInclusion;
     this.writeLongAsString = writeLongAsString;
+    this.byteArrayFormat = Objects.requireNonNull(byteArrayFormat, "byteArrayFormat");
     this.codegenEnabled = codegenEnabled;
     this.asyncCompilationEnabled = asyncCompilationEnabled;
     this.propertyDiscoveryEnabled = propertyDiscoveryEnabled;
@@ -109,6 +113,11 @@ public final class JsonConfig {
     return writeLongAsString;
   }
 
+  /** Returns the default byte-array representation for both reading and writing. */
+  public JsonByteArray.Format byteArrayFormat() {
+    return byteArrayFormat;
+  }
+
   public boolean codegenEnabled() {
     return codegenEnabled;
   }
diff --git a/java/fory-json/src/main/java/org/apache/fory/json/annotation/JsonByteArray.java b/java/fory-json/src/main/java/org/apache/fory/json/annotation/JsonByteArray.java
index b221093bd..6efd7cc30 100644
--- a/java/fory-json/src/main/java/org/apache/fory/json/annotation/JsonByteArray.java
+++ b/java/fory-json/src/main/java/org/apache/fory/json/annotation/JsonByteArray.java
@@ -27,7 +27,8 @@ import java.lang.annotation.Target;
 
 /**
  * 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.
+ * writing. Overrides {@link org.apache.fory.json.ForyJsonBuilder#byteArrayFormat(Format)}, whose
+ * default is a quoted standard Base64 string.
  *
  * <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
@@ -45,6 +46,8 @@ public @interface JsonByteArray {
     /** A quoted standard Base64 string with padding. */
     BASE64,
     /** A JSON array of signed byte values in the range {@code [-128, 127]}. */
-    ARRAY
+    ARRAY,
+    /** A quoted lowercase hexadecimal string. Reading also accepts uppercase hexadecimal digits. */
+    BASE16
   }
 }
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 99cfa784c..a560a4a3a 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
@@ -23,6 +23,7 @@ import java.lang.reflect.Array;
 import java.util.Arrays;
 import org.apache.fory.annotation.Internal;
 import org.apache.fory.json.ForyJsonException;
+import org.apache.fory.json.annotation.JsonByteArray;
 import org.apache.fory.json.reader.JsonReader;
 import org.apache.fory.json.reader.Latin1JsonReader;
 import org.apache.fory.json.reader.Utf16JsonReader;
@@ -67,11 +68,12 @@ public abstract class ArrayCodec<T> implements JsonValueCodec<T> {
     Class<?> componentType = arrayType.getComponentType();
     TypeRef<?> componentTypeRef = arrayTypeRef.getComponentType();
     JsonTypeInfo componentTypeInfo = resolver.getTypeInfo(componentTypeRef);
-    return create(arrayType, componentTypeInfo);
+    return create(arrayType, componentTypeInfo, resolver.sharedRegistry().byteArrayFormat());
   }
 
   @Internal
-  public static <T> JsonValueCodec<T> create(Class<T> arrayType, JsonTypeInfo componentTypeInfo) {
+  public static <T> JsonValueCodec<T> create(
+      Class<T> arrayType, JsonTypeInfo componentTypeInfo, JsonByteArray.Format byteArrayFormat) {
     if (!arrayType.isArray()) {
       throw new ForyJsonException("Unsupported JSON array type " + arrayType);
     }
@@ -91,7 +93,14 @@ 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(Base64ByteArrayCodec.INSTANCE);
+      switch (byteArrayFormat) {
+        case ARRAY:
+          return bind(ByteArrayCodec.SIGNED);
+        case BASE16:
+          return bind(Base16ByteArrayCodec.INSTANCE);
+        default:
+          return bind(Base64ByteArrayCodec.INSTANCE);
+      }
     } else if (componentType == char.class && componentCodec == ScalarCodecs.CharCodec.PRIMITIVE) {
       return bind(CharArrayCodec.INSTANCE);
     } else if (componentType == float.class
@@ -1237,10 +1246,30 @@ public abstract class ArrayCodec<T> implements JsonValueCodec<T> {
   }
 
   public abstract static class ByteArrayCodec extends ArrayCodec<byte[]> {
+    // Built-in byte elements parse directly from input, including quoted scalars. They cannot
+    // borrow the string decode buffer, so arrays may use it until the detached result is copied.
+    private static final long[] SIGNED_TOKENS = decimalTokens(false);
+    private static final long[] UNSIGNED_TOKENS = decimalTokens(true);
+    private static final ByteArrayCodec SIGNED = new SignedByteArrayCodec();
     private static final ByteArrayCodec UNSIGNED = new UnsignedByteArrayCodec();
+    private final long[] tokens;
 
-    private ByteArrayCodec() {
+    private ByteArrayCodec(long[] tokens) {
       super(byte.class);
+      this.tokens = tokens;
+    }
+
+    private static long[] decimalTokens(boolean unsigned) {
+      long[] tokens = new long[256];
+      for (int i = 0; i < tokens.length; i++) {
+        String text = Integer.toString(unsigned ? i : (byte) i) + ',';
+        long token = (long) text.length() << 56;
+        for (int j = 0; j < text.length(); j++) {
+          token |= (long) text.charAt(j) << (j << 3);
+        }
+        tokens[i] = token;
+      }
+      return tokens;
     }
 
     abstract void writeElement(StringJsonWriter writer, byte value);
@@ -1276,10 +1305,34 @@ public abstract class ArrayCodec<T> implements JsonValueCodec<T> {
       }
       byte[] array = value;
       writer.writeArrayStart();
-      for (int i = 0; i < array.length; i++) {
-        writer.writeComma(i);
-        writeElement(writer, array[i]);
+      if (writer.prettyPrint()) {
+        for (int i = 0; i < array.length; i++) {
+          writer.writeComma(i);
+          writeElement(writer, array[i]);
+        }
+        writer.writeArrayEnd();
+        return;
       }
+      byte[] bytes = writer.getBuffer();
+      int pos = writer.getPosition();
+      long[] tokens = this.tokens;
+      int i = 0;
+      while (i < array.length) {
+        int end = i + Math.min(64, array.length - i);
+        // Five bytes cover a signed byte and comma; the last wide store needs seven spare bytes.
+        int additional = (end - i) * 5 + Long.BYTES - 1;
+        if (additional > bytes.length - pos) {
+          writer.setPosition(pos);
+          writer.grow(additional);
+          bytes = writer.getBuffer();
+        }
+        for (; i < end; i++) {
+          long token = tokens[array[i] & 255];
+          LittleEndian.putInt64(bytes, pos, token);
+          pos += (int) (token >>> 56);
+        }
+      }
+      writer.setPosition(array.length == 0 ? pos : pos - 1);
       writer.writeArrayEnd();
     }
 
@@ -1294,15 +1347,14 @@ public abstract class ArrayCodec<T> implements JsonValueCodec<T> {
         finishPrimitiveArray(reader, 0, Byte.BYTES);
         return new byte[0];
       }
-      byte[] values = new byte[8];
+      byte[] values = reader.getStringDecodeBuffer();
       int size = 0;
       do {
-        rejectNull(reader);
         if ((size & ARRAY_BATCH_MASK) == ARRAY_BATCH_MASK) {
           reader.reserveGraphMemory(ARRAY_BATCH_SIZE * Byte.BYTES);
         }
         if (size == values.length) {
-          values = Arrays.copyOf(values, values.length << 1);
+          values = Arrays.copyOf(values, Math.max(8, values.length << 1));
         }
         values[size++] = readElement(reader);
       } while (reader.consumeNextToken(','));
@@ -1322,15 +1374,14 @@ public abstract class ArrayCodec<T> implements JsonValueCodec<T> {
         finishPrimitiveArray(reader, 0, Byte.BYTES);
         return new byte[0];
       }
-      byte[] values = new byte[8];
+      byte[] values = reader.getStringDecodeBuffer();
       int size = 0;
       do {
-        rejectNull(reader);
         if ((size & ARRAY_BATCH_MASK) == ARRAY_BATCH_MASK) {
           reader.reserveGraphMemory(ARRAY_BATCH_SIZE * Byte.BYTES);
         }
         if (size == values.length) {
-          values = Arrays.copyOf(values, values.length << 1);
+          values = Arrays.copyOf(values, Math.max(8, values.length << 1));
         }
         values[size++] = readElement(reader);
       } while (reader.consumeNextToken(','));
@@ -1350,15 +1401,14 @@ public abstract class ArrayCodec<T> implements JsonValueCodec<T> {
         finishPrimitiveArray(reader, 0, Byte.BYTES);
         return new byte[0];
       }
-      byte[] values = new byte[8];
+      byte[] values = reader.getStringDecodeBuffer();
       int size = 0;
       do {
-        rejectNull(reader);
         if ((size & ARRAY_BATCH_MASK) == ARRAY_BATCH_MASK) {
           reader.reserveGraphMemory(ARRAY_BATCH_SIZE * Byte.BYTES);
         }
         if (size == values.length) {
-          values = Arrays.copyOf(values, values.length << 1);
+          values = Arrays.copyOf(values, Math.max(8, values.length << 1));
         }
         values[size++] = readElement(reader);
       } while (reader.consumeNextToken(','));
@@ -1370,7 +1420,9 @@ public abstract class ArrayCodec<T> implements JsonValueCodec<T> {
 
   /** A complete {@code byte[]} codec using a JSON array of signed byte values. */
   public static final class SignedByteArrayCodec extends ByteArrayCodec {
-    public SignedByteArrayCodec() {}
+    public SignedByteArrayCodec() {
+      super(ByteArrayCodec.SIGNED_TOKENS);
+    }
 
     @Override
     void writeElement(StringJsonWriter writer, byte value) {
@@ -1399,6 +1451,10 @@ public abstract class ArrayCodec<T> implements JsonValueCodec<T> {
   }
 
   private static final class UnsignedByteArrayCodec extends ByteArrayCodec {
+    private UnsignedByteArrayCodec() {
+      super(ByteArrayCodec.UNSIGNED_TOKENS);
+    }
+
     @Override
     void writeElement(StringJsonWriter writer, byte value) {
       writer.writeInt(Byte.toUnsignedInt(value));
@@ -3738,30 +3794,6 @@ public abstract class ArrayCodec<T> implements JsonValueCodec<T> {
     writer.writeNull();
   }
 
-  private static void rejectNull(JsonReader reader) {
-    if (reader.tryReadNull()) {
-      throw new ForyJsonException("Cannot read null into primitive array element");
-    }
-  }
-
-  private static void rejectNull(Latin1JsonReader reader) {
-    if (reader.tryReadNullToken()) {
-      throw new ForyJsonException("Cannot read null into primitive array element");
-    }
-  }
-
-  private static void rejectNull(Utf16JsonReader reader) {
-    if (reader.tryReadNullToken()) {
-      throw new ForyJsonException("Cannot read null into primitive array element");
-    }
-  }
-
-  private static void rejectNull(Utf8JsonReader reader) {
-    if (reader.tryReadNullToken()) {
-      throw new ForyJsonException("Cannot read null into primitive array element");
-    }
-  }
-
   private static short readShort(int value) {
     if (value < Short.MIN_VALUE || value > Short.MAX_VALUE) {
       throw new ForyJsonException("Short overflow");
@@ -3770,7 +3802,7 @@ public abstract class ArrayCodec<T> implements JsonValueCodec<T> {
   }
 
   private static byte readByte(int value) {
-    if (value < Byte.MIN_VALUE || value > Byte.MAX_VALUE) {
+    if (value != (byte) value) {
       throw new ForyJsonException("Byte overflow");
     }
     return (byte) value;
diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codec/Base16ByteArrayCodec.java b/java/fory-json/src/main/java/org/apache/fory/json/codec/Base16ByteArrayCodec.java
new file mode 100644
index 000000000..46f803d82
--- /dev/null
+++ b/java/fory-json/src/main/java/org/apache/fory/json/codec/Base16ByteArrayCodec.java
@@ -0,0 +1,66 @@
+/*
+ * 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.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;
+
+/** A complete {@code byte[]} codec using a quoted lowercase hexadecimal JSON string. */
+public final class Base16ByteArrayCodec implements JsonValueCodec<byte[]> {
+  public static final Base16ByteArrayCodec INSTANCE = new Base16ByteArrayCodec();
+
+  public Base16ByteArrayCodec() {}
+
+  @Override
+  public void writeString(StringJsonWriter writer, byte[] value) {
+    if (value == null) {
+      writer.writeNull();
+    } else {
+      writer.writeBase16(value);
+    }
+  }
+
+  @Override
+  public void writeUtf8(Utf8JsonWriter writer, byte[] value) {
+    if (value == null) {
+      writer.writeNull();
+    } else {
+      writer.writeBase16(value);
+    }
+  }
+
+  @Override
+  public byte[] readLatin1(Latin1JsonReader reader) {
+    return reader.readBase16();
+  }
+
+  @Override
+  public byte[] readUtf16(Utf16JsonReader reader) {
+    return reader.readBase16();
+  }
+
+  @Override
+  public byte[] readUtf8(Utf8JsonReader reader) {
+    return reader.readBase16();
+  }
+}
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 dfc9ba2a8..fd7e2a388 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
@@ -3573,7 +3573,9 @@ final class ObjectCodecBuilder {
         Class<? extends JsonValueCodec<?>> codecClass =
             byteArray.value() == JsonByteArray.Format.ARRAY
                 ? ArrayCodec.SignedByteArrayCodec.class
-                : Base64ByteArrayCodec.class;
+                : byteArray.value() == JsonByteArray.Format.BASE16
+                    ? Base16ByteArrayCodec.class
+                    : Base64ByteArrayCodec.class;
         if (valueCodecClass != null && valueCodecClass != codecClass) {
           throw new ForyJsonException(
               "Conflicting @JsonByteArray declarations for property " + name);
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 cc9a99052..62841c326 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
@@ -523,6 +523,81 @@ public abstract class JsonReader {
     }
   }
 
+  /** Reads a nullable hexadecimal JSON string, accepting uppercase and lowercase digits. */
+  public byte[] readBase16() {
+    if (tryReadNullToken()) {
+      return null;
+    }
+    int start = position + 1;
+    int end = scanStringEnd(position) - 1;
+    position = end + 1;
+    int length = end - start;
+    if (length == 0) {
+      return EMPTY_BYTES;
+    }
+    if ((length & 1) != 0) {
+      return decodeBase16(decodeQuotedText(start, end));
+    }
+    byte[] decoded = new byte[length >>> 1];
+    for (int i = 0; i < decoded.length; i++) {
+      char first = charAt(start + (i << 1));
+      char second = charAt(start + (i << 1) + 1);
+      if ((first | second) > 255) {
+        throw error("Invalid hex digit");
+      }
+      int value = HEX_PAIRS[first | (second << 8)];
+      if (value < 0) {
+        return decodeBase16(decodeQuotedText(start, end));
+      }
+      decoded[i] = (byte) value;
+    }
+    return decoded;
+  }
+
+  /** Decodes a proven quoted byte range directly; escaped strings use the common decoder. */
+  protected final byte[] readBase16(byte[] bytes) {
+    if (tryReadNullToken()) {
+      return null;
+    }
+    int start = position + 1;
+    int end = scanStringEnd(position) - 1;
+    position = end + 1;
+    int length = end - start;
+    if (length == 0) {
+      return EMPTY_BYTES;
+    }
+    if ((length & 1) != 0) {
+      return decodeBase16(decodeQuotedText(start, end));
+    }
+    // The scan proves the complete source range before allocating binary storage.
+    byte[] decoded = new byte[length >>> 1];
+    short[] pairs = HEX_PAIRS;
+    for (int i = 0; i < decoded.length; i++) {
+      int offset = start + (i << 1);
+      int value = pairs[(bytes[offset] & 255) | ((bytes[offset + 1] & 255) << 8)];
+      if (value < 0) {
+        return decodeBase16(decodeQuotedText(start, end));
+      }
+      decoded[i] = (byte) value;
+    }
+    return decoded;
+  }
+
+  private byte[] decodeBase16(CharSequence encoded) {
+    int encodedLength = encoded.length();
+    if ((encodedLength & 1) != 0) {
+      throw error("Invalid Base16 JSON string length");
+    }
+    if (encodedLength == 0) {
+      return EMPTY_BYTES;
+    }
+    byte[] decoded = new byte[encodedLength >>> 1];
+    for (int i = 0, j = 0; i < decoded.length; i++, j += 2) {
+      decoded[i] = (byte) ((hexValue(encoded.charAt(j)) << 4) | hexValue(encoded.charAt(j + 1)));
+    }
+    return decoded;
+  }
+
   public String readCharSequence() {
     return readString();
   }
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 ac6c5b48f..86c453613 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
@@ -164,6 +164,11 @@ public final class Latin1JsonReader extends JsonReader {
     throw errorAt("Unterminated string", cursor);
   }
 
+  @Override
+  public byte[] readBase16() {
+    return readBase16(input);
+  }
+
   @Override
   protected long scanStringHash(int start, int end) {
     long hash = JsonFieldNameHash.MAGIC_HASH_CODE;
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 1545751d4..828deeee7 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
@@ -221,6 +221,11 @@ public final class Utf8JsonReader extends JsonReader {
     throw errorAt("Unterminated string", cursor);
   }
 
+  @Override
+  public byte[] readBase16() {
+    return readBase16(input);
+  }
+
   @Override
   protected long scanStringHash(int start, int end) {
     long hash = JsonFieldNameHash.MAGIC_HASH_CODE;
@@ -928,13 +933,13 @@ public final class Utf8JsonReader extends JsonReader {
     offset++;
     int safeEnd = Math.min(offset + 8, inputLimit);
     while (offset < safeEnd) {
-      ch = bytes[offset];
-      if (ch < '0' || ch > '9') {
+      int digit = bytes[offset] - '0';
+      if (Integer.compareUnsigned(digit, 9) > 0) {
         // A non-digit already ends the magnitude; only exhausting the digit bound needs the tail.
         position = offset;
         return negative ? -result : result;
       }
-      result = result * 10 + (ch - '0');
+      result = result * 10 + digit;
       offset++;
     }
     if (offset < inputLimit) {
@@ -4797,7 +4802,7 @@ public final class Utf8JsonReader extends JsonReader {
   private void rejectFractionOrExponentFast() {
     if (position < inputLimit) {
       int ch = input[position];
-      if (ch == '.' || ch == 'e' || ch == 'E') {
+      if (ch == '.' || (ch | 0x20) == 'e') {
         throw error("Expected integer");
       }
     }
diff --git a/java/fory-json/src/main/java/org/apache/fory/json/resolver/GeneratedCodecKeyBuilder.java b/java/fory-json/src/main/java/org/apache/fory/json/resolver/GeneratedCodecKeyBuilder.java
index d6c1c8f01..1d7c3689e 100644
--- a/java/fory-json/src/main/java/org/apache/fory/json/resolver/GeneratedCodecKeyBuilder.java
+++ b/java/fory-json/src/main/java/org/apache/fory/json/resolver/GeneratedCodecKeyBuilder.java
@@ -77,6 +77,8 @@ final class GeneratedCodecKeyBuilder {
       keyParts.add(registry.defaultPropertyInclusion());
       keyParts.add(registry.writeLongAsString());
     }
+    // Both readers and writers store concrete byte-array codecs selected by this default.
+    keyParts.add(registry.byteArrayFormat());
     keyParts.add(registry.propertyDiscoveryEnabled());
     keyParts.add(registry.propertyNamingStrategy());
     addModelInputs(typeInfo, owner.unwrappedInfo(), registry);
diff --git a/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonSharedRegistry.java b/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonSharedRegistry.java
index 940370af3..2cbd98e27 100644
--- a/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonSharedRegistry.java
+++ b/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonSharedRegistry.java
@@ -102,6 +102,7 @@ import org.apache.fory.json.JsonConfig;
 import org.apache.fory.json.JsonTypeCheckContext;
 import org.apache.fory.json.JsonTypeChecker;
 import org.apache.fory.json.PropertyNamingStrategy;
+import org.apache.fory.json.annotation.JsonByteArray;
 import org.apache.fory.json.annotation.JsonCodec;
 import org.apache.fory.json.annotation.JsonProperty.Include;
 import org.apache.fory.json.annotation.JsonSubTypes;
@@ -189,6 +190,7 @@ public final class JsonSharedRegistry {
   private final PropertyNamingStrategy propertyNamingStrategy;
   private final Include defaultPropertyInclusion;
   private final boolean writeLongAsString;
+  private final JsonByteArray.Format byteArrayFormat;
   private final ClassLoader classLoader;
   private final JsonMixinAnnotations mixinAnnotations;
   private final IdentityHashMap<Class<?>, JsonSubTypesInfo> subTypesCache;
@@ -246,6 +248,7 @@ public final class JsonSharedRegistry {
     propertyNamingStrategy = config.propertyNamingStrategy();
     defaultPropertyInclusion = config.defaultPropertyInclusion();
     writeLongAsString = config.writeLongAsString();
+    byteArrayFormat = config.byteArrayFormat();
     classLoader = config.classLoader();
     mixinAnnotations = new JsonMixinAnnotations(config);
     exactCodecs = new IdentityHashMap<>();
@@ -1227,6 +1230,12 @@ public final class JsonSharedRegistry {
     return writeLongAsString;
   }
 
+  /** Returns the default representation used when selecting ordinary byte-array codecs. */
+  @Internal
+  public JsonByteArray.Format byteArrayFormat() {
+    return byteArrayFormat;
+  }
+
   ClassLoader classLoader() {
     return classLoader;
   }
diff --git a/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonTypeResolver.java b/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonTypeResolver.java
index 4a5f8c440..bec684578 100644
--- a/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonTypeResolver.java
+++ b/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonTypeResolver.java
@@ -558,7 +558,8 @@ public final class JsonTypeResolver {
       TypeRef<?> elementType = typeRef.getComponentType();
       requireConcreteChild(elementType.getType(), rawType, "elementCodec");
       JsonTypeInfo elementInfo = annotationTypeInfo(elementType, elementCodec);
-      return newTypeInfo(declaredType, ArrayCodec.create(rawType, elementInfo));
+      return newTypeInfo(
+          declaredType, ArrayCodec.create(rawType, elementInfo, sharedRegistry.byteArrayFormat()));
     }
     if (rawType == AtomicReferenceArray.class) {
       requireSlots(rawType, hasElement, !hasContent && !hasKey && !hasMapValue, "elementCodec");
@@ -643,7 +644,8 @@ public final class JsonTypeResolver {
       TypeRef<?> elementType = typeRef.getComponentType();
       requireConcreteChild(elementType.getType(), rawType, "element", "@JsonFormat");
       JsonTypeInfo elementInfo = formatTypeInfo(elementType, annotation);
-      return newTypeInfo(declaredType, ArrayCodec.create(rawType, elementInfo));
+      return newTypeInfo(
+          declaredType, ArrayCodec.create(rawType, elementInfo, sharedRegistry.byteArrayFormat()));
     }
     if (rawType == AtomicReferenceArray.class) {
       TypeRef<?> elementType = directElementType(typeRef, rawType, "element", "@JsonFormat");
diff --git a/java/fory-json/src/main/java/org/apache/fory/json/writer/JsonWriter.java b/java/fory-json/src/main/java/org/apache/fory/json/writer/JsonWriter.java
index 2e1ce6791..b4039b4f9 100644
--- a/java/fory-json/src/main/java/org/apache/fory/json/writer/JsonWriter.java
+++ b/java/fory-json/src/main/java/org/apache/fory/json/writer/JsonWriter.java
@@ -57,6 +57,22 @@ import org.apache.fory.json.resolver.JsonTypeResolver;
  * reparsing.
  */
 public abstract class JsonWriter {
+  // Initialized only for Base16 writes, and shared by both output representations.
+  protected static final class HexDigits {
+    static final int[] QUADS = new int[65536];
+
+    static {
+      String digits = "0123456789abcdef";
+      for (int i = 0; i < QUADS.length; i++) {
+        QUADS[i] =
+            digits.charAt((i >>> 4) & 15)
+                | (digits.charAt(i & 15) << 8)
+                | (digits.charAt(i >>> 12) << 16)
+                | (digits.charAt((i >>> 8) & 15) << 24);
+      }
+    }
+  }
+
   private static final long MIN_ISO_INSTANT_SECOND = -31_557_014_167_219_200L;
   private static final long MAX_ISO_INSTANT_SECOND = 31_556_889_864_403_199L;
   private final JsonTypeResolver typeResolver;
diff --git a/java/fory-json/src/main/java/org/apache/fory/json/writer/StringJsonWriter.java b/java/fory-json/src/main/java/org/apache/fory/json/writer/StringJsonWriter.java
index f08506216..f681dba21 100644
--- a/java/fory-json/src/main/java/org/apache/fory/json/writer/StringJsonWriter.java
+++ b/java/fory-json/src/main/java/org/apache/fory/json/writer/StringJsonWriter.java
@@ -80,6 +80,7 @@ public final class StringJsonWriter extends JsonWriter implements Appendable {
   private static final long DECIMAL_8 = 100_000_000L;
   private static final int[] DIGIT_TRIPLES = new int[1000];
   private static final int[] DIGIT_QUADS = new int[10000];
+  private static final int[] HEX_PAIRS = new int[256];
   private static final long[] UTF16_DIGIT_QUADS = new long[10000];
   private static final long UTF16_BYTE_MASK = 0x00FF00FF00FF00FFL;
   private static final long UTF16_PAIR_MASK = 0x0000FFFF0000FFFFL;
@@ -96,6 +97,10 @@ public final class StringJsonWriter extends JsonWriter implements Appendable {
   private static final boolean LITTLE_ENDIAN = NativeByteOrder.IS_LITTLE_ENDIAN;
 
   static {
+    String hexDigits = "0123456789abcdef";
+    for (int i = 0; i < HEX_PAIRS.length; i++) {
+      HEX_PAIRS[i] = hexDigits.charAt(i >>> 4) | (hexDigits.charAt(i & 15) << 8);
+    }
     for (int i = 0; i < 1000; i++) {
       int c0 = '0' + i / 100;
       int c1 = '0' + (i / 10) % 10;
@@ -1560,6 +1565,54 @@ public final class StringJsonWriter extends JsonWriter implements Appendable {
     writeRawUtf16Value(index == 0 ? utf16NamePrefix : utf16CommaNamePrefix);
   }
 
+  /** Writes a byte array as a quoted lowercase hexadecimal string without intermediate storage. */
+  public void writeBase16(byte[] value) {
+    int pos = position;
+    long additional = (value.length * 2L + 2) << coder;
+    if (additional > Integer.MAX_VALUE - (long) pos) {
+      throw new ForyJsonException("Byte array is too large for Base16 JSON output");
+    }
+    if (pos + additional > buffer.length) {
+      grow((int) additional);
+    }
+    byte[] target = buffer;
+    int[] words = HexDigits.QUADS;
+    if (coder == LATIN1) {
+      target[pos++] = '"';
+      int index = 0;
+      for (; index <= value.length - 2; index += 2) {
+        int bits = (value[index] & 0xff) | ((value[index + 1] & 0xff) << 8);
+        LittleEndian.putInt32(target, pos, words[bits]);
+        pos += 4;
+      }
+      if (index < value.length) {
+        int pair = HEX_PAIRS[value[index] & 0xff];
+        target[pos++] = (byte) pair;
+        target[pos++] = (byte) (pair >>> 8);
+      }
+      target[pos++] = '"';
+    } else {
+      pos = putUtf16Byte(target, pos, (byte) '"');
+      int index = 0;
+      for (; index <= value.length - 2; index += 2) {
+        int bits = (value[index] & 0xff) | ((value[index + 1] & 0xff) << 8);
+        long chars = words[bits] & 0xffffffffL;
+        chars = (chars | (chars << 16)) & 0x0000ffff0000ffffL;
+        chars = (chars | (chars << 8)) & 0x00ff00ff00ff00ffL;
+        LittleEndian.putInt64(target, pos, LITTLE_ENDIAN ? chars : chars << 8);
+        pos += 8;
+      }
+      if (index < value.length) {
+        int pair = HEX_PAIRS[value[index] & 0xff];
+        int chars = (pair & 0xff) | ((pair & 0xff00) << 8);
+        LittleEndian.putInt32(target, pos, LITTLE_ENDIAN ? chars : chars << 8);
+        pos += 4;
+      }
+      pos = putUtf16Byte(target, pos, (byte) '"');
+    }
+    position = pos;
+  }
+
   /** Writes a byte array as a quoted Base64 JSON string without an intermediate String. */
   public void writeBase64(byte[] value) {
     int encodedLength = base64Length(value.length);
diff --git a/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java b/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java
index 168800aa9..fdfb0e332 100644
--- a/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java
+++ b/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java
@@ -1924,6 +1924,34 @@ public final class Utf8JsonWriter extends JsonWriter implements Appendable {
     }
   }
 
+  /** Writes a byte array as a quoted lowercase hexadecimal string without intermediate storage. */
+  public void writeBase16(byte[] value) {
+    int pos = position;
+    long additional = value.length * 2L + 2;
+    if (additional > Integer.MAX_VALUE - (long) pos) {
+      throw new ForyJsonException("Byte array is too large for Base16 JSON output");
+    }
+    if (pos + additional > buffer.length) {
+      grow((int) additional);
+    }
+    byte[] target = buffer;
+    target[pos++] = '"';
+    int[] words = HexDigits.QUADS;
+    int index = 0;
+    for (; index <= value.length - 2; index += 2) {
+      int bits = (value[index] & 0xff) | ((value[index + 1] & 0xff) << 8);
+      LittleEndian.putInt32(target, pos, words[bits]);
+      pos += 4;
+    }
+    if (index < value.length) {
+      int pair = HEX_PAIRS[value[index] & 0xff];
+      target[pos++] = (byte) pair;
+      target[pos++] = (byte) (pair >>> 8);
+    }
+    target[pos++] = '"';
+    position = pos;
+  }
+
   /** Writes a byte array as a quoted Base64 JSON string without an intermediate String. */
   public void writeBase64(byte[] value) {
     int encodedLength = base64Length(value.length);
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 d5f986899..e575bda4c 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
@@ -61,6 +61,7 @@ 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.Base16ByteArrayCodec;
 import org.apache.fory.json.codec.Base64ByteArrayCodec;
 import org.apache.fory.json.codec.JsonUnwrappedInfo;
 import org.apache.fory.json.codec.ObjectCodec;
@@ -1005,7 +1006,9 @@ final class ForyJsonGraalVMFeature implements Feature {
       registerCodec(
           byteArray.value() == JsonByteArray.Format.ARRAY
               ? ArrayCodec.SignedByteArrayCodec.class
-              : Base64ByteArrayCodec.class);
+              : byteArray.value() == JsonByteArray.Format.BASE16
+                  ? Base16ByteArrayCodec.class
+                  : Base64ByteArrayCodec.class);
     }
   }
 
diff --git a/java/fory-json/src/main/resources/META-INF/native-image/org.apache.fory/fory-json/native-image.properties b/java/fory-json/src/main/resources/META-INF/native-image/org.apache.fory/fory-json/native-image.properties
index a14675575..58f794d23 100644
--- a/java/fory-json/src/main/resources/META-INF/native-image/org.apache.fory/fory-json/native-image.properties
+++ b/java/fory-json/src/main/resources/META-INF/native-image/org.apache.fory/fory-json/native-image.properties
@@ -29,6 +29,7 @@ Args=--features=org.apache.fory.json.ForyJsonGraalVMFeature \
     org.apache.fory.json.JsonConfig,\
     org.apache.fory.json.JsonTypeCheckContext,\
     org.apache.fory.json.PropertyNamingStrategy,\
+    org.apache.fory.json.annotation.JsonByteArray$Format,\
     org.apache.fory.json.annotation.JsonProperty$Include,\
     org.apache.fory.util.function.ObjBooleanConsumer,\
     org.apache.fory.util.function.ObjByteConsumer,\
diff --git a/java/fory-json/src/test/java/org/apache/fory/json/ForyJsonGraalVMFeatureJarVerifier.java b/java/fory-json/src/test/java/org/apache/fory/json/ForyJsonGraalVMFeatureJarVerifier.java
index 02fa3a501..1bb3d669a 100644
--- a/java/fory-json/src/test/java/org/apache/fory/json/ForyJsonGraalVMFeatureJarVerifier.java
+++ b/java/fory-json/src/test/java/org/apache/fory/json/ForyJsonGraalVMFeatureJarVerifier.java
@@ -63,6 +63,7 @@ public final class ForyJsonGraalVMFeatureJarVerifier {
           + "org.apache.fory.json.JsonConfig,"
           + "org.apache.fory.json.JsonTypeCheckContext,"
           + "org.apache.fory.json.PropertyNamingStrategy,"
+          + "org.apache.fory.json.annotation.JsonByteArray$Format,"
           + "org.apache.fory.json.annotation.JsonProperty$Include,"
           + "org.apache.fory.util.function.ObjBooleanConsumer,"
           + "org.apache.fory.util.function.ObjByteConsumer,"
diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonAsyncCompilationTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonAsyncCompilationTest.java
index 4b9126779..62cd018fd 100644
--- a/java/fory-json/src/test/java/org/apache/fory/json/JsonAsyncCompilationTest.java
+++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonAsyncCompilationTest.java
@@ -57,6 +57,7 @@ import java.util.concurrent.atomic.AtomicInteger;
 import java.util.concurrent.atomic.AtomicReference;
 import org.apache.fory.collection.IdentityMap;
 import org.apache.fory.json.annotation.JsonAnyProperty;
+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.JsonProperty.Include;
@@ -1366,6 +1367,7 @@ public class JsonAsyncCompilationTest {
         new JsonConfig(
             Include.NON_NULL,
             false,
+            JsonByteArray.Format.BASE64,
             true,
             true,
             true,
diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonByteArrayAnnotationTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonByteArrayAnnotationTest.java
index a069b443f..2693a9c71 100644
--- a/java/fory-json/src/test/java/org/apache/fory/json/JsonByteArrayAnnotationTest.java
+++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonByteArrayAnnotationTest.java
@@ -24,6 +24,10 @@ import static org.testng.Assert.assertNull;
 import static org.testng.Assert.assertThrows;
 
 import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
 import java.util.Map;
 import org.apache.fory.json.annotation.JsonAnyGetter;
 import org.apache.fory.json.annotation.JsonAnyProperty;
@@ -31,11 +35,13 @@ 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;
+import org.apache.fory.json.annotation.JsonMixin;
 import org.apache.fory.json.annotation.JsonProperty;
 import org.apache.fory.json.annotation.JsonPropertyOrder;
 import org.apache.fory.json.annotation.JsonRawValue;
 import org.apache.fory.json.codec.Base64ByteArrayCodec;
 import org.apache.fory.platform.JdkVersion;
+import org.apache.fory.reflect.TypeRef;
 import org.testng.SkipException;
 import org.testng.annotations.Factory;
 import org.testng.annotations.Test;
@@ -46,6 +52,186 @@ public class JsonByteArrayAnnotationTest extends ForyJsonTestModels {
     super(codegen);
   }
 
+  @Test
+  public void globalFormats() {
+    byte[] bytes = {0, 127, -128, -1};
+    String[] encodings = {"\"AH+A/w==\"", "\"007f80ff\"", "[0,127,-128,-1]"};
+    JsonByteArray.Format[] formats = {
+      JsonByteArray.Format.BASE64, JsonByteArray.Format.BASE16, JsonByteArray.Format.ARRAY
+    };
+    ForyJsonBuilder builder = newJsonBuilder();
+    ForyJson defaultJson = builder.build();
+    for (int i = 0; i < formats.length; i++) {
+      ForyJson json = builder.byteArrayFormat(formats[i]).build();
+      String encoded = encodings[i];
+      assertEquals(json.toJson(bytes), encoded);
+      assertEquals(json.toJson(bytes, TypeRef.of(Object.class)), encoded);
+      assertEquals(new String(json.toJsonBytes(bytes), StandardCharsets.UTF_8), encoded);
+      assertEquals(json.fromJson(encoded, byte[].class), bytes);
+      assertEquals(json.fromJson(encoded.getBytes(StandardCharsets.UTF_8), byte[].class), bytes);
+      assertNull(json.fromJson("null", byte[].class));
+      assertNull(json.fromJson("null".getBytes(StandardCharsets.UTF_8), byte[].class));
+
+      BinaryValues value = new BinaryValues();
+      value.bytes = bytes;
+      value.list = Arrays.asList(bytes, null);
+      value.map = Collections.singletonMap("data", bytes);
+      value.nested = new byte[][] {bytes};
+      String expected =
+          "{\"label\":\"汉\",\"bytes\":"
+              + encoded
+              + ",\"list\":["
+              + encoded
+              + ",null],\"map\":{\"data\":"
+              + encoded
+              + "},\"nested\":["
+              + encoded
+              + "]}";
+      assertEquals(json.toJson(value), expected);
+      assertEquals(new String(json.toJsonBytes(value), StandardCharsets.UTF_8), expected);
+      String pretty = json.toPrettyJson(value);
+      assertEquals(new String(json.toPrettyJsonBytes(value), StandardCharsets.UTF_8), pretty);
+      for (String text : new String[] {expected, pretty}) {
+        assertBinaryValues(json.fromJson(text, BinaryValues.class), bytes);
+        assertBinaryValues(
+            json.fromJson(text.getBytes(StandardCharsets.UTF_8), BinaryValues.class), bytes);
+      }
+      TypeRef<List<byte[]>> listType = new TypeRef<List<byte[]>>() {};
+      assertEquals(json.toJson(value.list, listType), "[" + encoded + ",null]");
+      assertEquals(json.fromJson("[" + encoded + ",null]", listType).get(0), bytes);
+      TypeRef<Map<String, byte[]>> mapType = new TypeRef<Map<String, byte[]>>() {};
+      assertEquals(json.toJson(value.map, mapType), "{\"data\":" + encoded + "}");
+      assertEquals(json.fromJson("{\"data\":" + encoded + "}", mapType).get("data"), bytes);
+      assertGeneratedWhenSupported(json, BinaryValues.class, codegenEnabled());
+    }
+    assertEquals(defaultJson.toJson(bytes), encodings[0]);
+    assertThrows(NullPointerException.class, () -> builder.byteArrayFormat(null));
+  }
+
+  private static void assertBinaryValues(BinaryValues value, byte[] bytes) {
+    assertEquals(value.label, "汉");
+    assertEquals(value.bytes, bytes);
+    assertEquals(value.list.get(0), bytes);
+    assertNull(value.list.get(1));
+    assertEquals(value.map.get("data"), bytes);
+    assertEquals(value.nested[0], bytes);
+  }
+
+  @Test
+  public void base16Overrides() {
+    ForyJson json = newJsonBuilder().byteArrayFormat(JsonByteArray.Format.BASE16).build();
+    Base64Field base64 = new Base64Field();
+    base64.bytes = new byte[] {1, -2};
+    assertEquals(json.toJson(base64), "{\"bytes\":\"Af4=\"}");
+    assertEquals(json.fromJson(json.toJsonBytes(base64), Base64Field.class).bytes, base64.bytes);
+    ArrayField array = new ArrayField();
+    array.bytes = base64.bytes;
+    assertEquals(json.toJson(array), "{\"bytes\":[1,-2]}");
+    assertEquals(json.fromJson(json.toJsonBytes(array), ArrayField.class).bytes, base64.bytes);
+    DirectCodecBase64 custom = new DirectCodecBase64();
+    custom.bytes = base64.bytes;
+    assertEquals(json.toJson(custom), "{\"bytes\":\"Af4=\"}");
+
+    ForyJson mixinJson =
+        newJsonBuilder()
+            .byteArrayFormat(JsonByteArray.Format.ARRAY)
+            .registerMixin(HexMixin.class)
+            .build();
+    assertEquals(mixinJson.toJson(base64), "{\"bytes\":\"01fe\"}");
+    assertEquals(
+        mixinJson.fromJson(mixinJson.toJsonBytes(base64), Base64Field.class).bytes, base64.bytes);
+    assertGeneratedWhenSupported(mixinJson, Base64Field.class, codegenEnabled());
+
+    HexField hex = new HexField();
+    hex.bytes = base64.bytes;
+    assertEquals(newJson().toJson(hex), "{\"bytes\":\"01fe\"}");
+    hex.bytes = null;
+    assertEquals(newJson().toJson(hex), "{}");
+    assertEquals(newJsonBuilder().writeNullFields(true).build().toJson(hex), "{\"bytes\":null}");
+  }
+
+  @Test
+  public void base16Contents() {
+    ForyJson json = newJsonBuilder().byteArrayFormat(JsonByteArray.Format.BASE16).build();
+    for (int size : new int[] {0, 1, 2, 3, 4, 7, 8, 9, 255, 256, 257, 1025, 131072}) {
+      byte[] bytes = new byte[size];
+      StringBuilder expected = new StringBuilder("\"");
+      for (int i = 0; i < size; i++) {
+        // Cover every two-byte combination as well as odd tails and buffer growth.
+        int value = size == 131072 ? (i >>> ((i & 1) == 0 ? 1 : 9)) & 255 : i & 255;
+        bytes[i] = (byte) value;
+        expected.append(Character.forDigit(value >>> 4, 16));
+        expected.append(Character.forDigit(value & 15, 16));
+      }
+      String text = expected.append('"').toString();
+      assertEquals(json.toJson(bytes), text);
+      assertEquals(new String(json.toJsonBytes(bytes), StandardCharsets.UTF_8), text);
+      for (String input : new String[] {text, text.toUpperCase(Locale.ROOT)}) {
+        assertEquals(json.fromJson(input, byte[].class), bytes);
+        assertEquals(json.fromJson(input.getBytes(StandardCharsets.UTF_8), byte[].class), bytes);
+      }
+      // The prefix widens String output and forces UTF16 input before the binary value.
+      String unicode = "{\"label\":\"汉\",\"bytes\":" + text + "}";
+      UnicodeHex value = new UnicodeHex();
+      value.bytes = bytes;
+      assertEquals(json.toJson(value), unicode);
+      assertEquals(json.fromJson(unicode, UnicodeHex.class).bytes, bytes);
+    }
+    for (String escaped : new String[] {"\"0\\u0061Ff\"", "\"\\u0030\\u0061Ff\""}) {
+      assertEquals(json.fromJson(escaped, byte[].class), new byte[] {10, -1});
+      assertEquals(
+          json.fromJson(escaped.getBytes(StandardCharsets.UTF_8), byte[].class),
+          new byte[] {10, -1});
+      String unicode = "{\"label\":\"汉\",\"bytes\":" + escaped + "}";
+      assertEquals(json.fromJson(unicode, UnicodeHex.class).bytes, new byte[] {10, -1});
+      assertEquals(
+          json.fromJson(unicode.getBytes(StandardCharsets.UTF_8), UnicodeHex.class).bytes,
+          new byte[] {10, -1});
+    }
+    for (String input :
+        new String[] {
+          "\"0\"", "\"gg\"", "\"0g\"", "\"汉0\"", "\"0 00\"", "\"00", "[0]", "\"\\u0030\""
+        }) {
+      assertThrows(ForyJsonException.class, () -> json.fromJson(input, byte[].class));
+      assertThrows(
+          ForyJsonException.class,
+          () -> json.fromJson(input.getBytes(StandardCharsets.UTF_8), byte[].class));
+      assertEquals(json.fromJson("\"00\"", byte[].class), new byte[] {0});
+    }
+    ForyJson leaf =
+        newJsonBuilder()
+            .byteArrayFormat(JsonByteArray.Format.BASE16)
+            .withMaxGraphMemoryBytes(1)
+            .build();
+    assertEquals(leaf.fromJson(leaf.toJsonBytes(new byte[1024]), byte[].class), new byte[1024]);
+  }
+
+  @JsonPropertyOrder({"label", "bytes", "list", "map", "nested"})
+  public static final class BinaryValues {
+    public String label = "汉";
+    public byte[] bytes;
+    public List<byte[]> list;
+    public Map<String, byte[]> map;
+    public byte[][] nested;
+  }
+
+  public static final class HexField {
+    @JsonByteArray(JsonByteArray.Format.BASE16)
+    public byte[] bytes;
+  }
+
+  @JsonPropertyOrder({"label", "bytes"})
+  public static final class UnicodeHex {
+    public String label = "汉";
+    public byte[] bytes;
+  }
+
+  @JsonMixin(target = Base64Field.class)
+  public abstract static class HexMixin {
+    @JsonByteArray(JsonByteArray.Format.BASE16)
+    public byte[] bytes;
+  }
+
   @Test
   public void arrayRoundTrip() {
     ForyJson json = newJson();
@@ -82,6 +268,56 @@ public class JsonByteArrayAnnotationTest extends ForyJsonTestModels {
     assertEquals(json.fromJson("{\"bytes\":[1,-2]}", ArrayGetter.class).bytes, value.bytes);
   }
 
+  @Test
+  public void arrayContents() {
+    ForyJson json = newJsonBuilder().byteArrayFormat(JsonByteArray.Format.ARRAY).build();
+    for (int size : new int[] {0, 7, 8, 9, 1023, 1024, 1025, 4097}) {
+      byte[] expected = new byte[size];
+      StringBuilder array = new StringBuilder("[");
+      for (int i = 0; i < size; i++) {
+        expected[i] = (byte) i;
+        if (i != 0) {
+          array.append(",\n");
+        }
+        array.append('"').append(expected[i]).append('"');
+      }
+      String text = array.append(']').toString();
+      String compact = Arrays.toString(expected).replace(" ", "");
+      assertEquals(new String(json.toJsonBytes(expected), StandardCharsets.UTF_8), compact);
+      assertEquals(json.fromJson(json.toPrettyJsonBytes(expected), byte[].class), expected);
+      byte[] first = json.fromJson(text, byte[].class);
+      byte[] second = json.fromJson(text.getBytes(StandardCharsets.UTF_8), byte[].class);
+      String nested = "{\"ignored\":\"汉\\u6c49\",\"bytes\":" + text + "}";
+      assertEquals(json.fromJson(nested, ArrayField.class).bytes, expected);
+      json.fromJson("[-1,-2,-3]", byte[].class);
+      assertEquals(first, expected);
+      assertEquals(second, expected);
+    }
+    String text =
+        "{\"label\":\"汉\\u6c49\",\"bytes\":[1,2],\"list\":[[3],[4,5]],"
+            + "\"map\":{\"a\":[6],\"b\":[7,8]},\"nested\":[[9],[10,11]]}";
+    for (BinaryValues value :
+        new BinaryValues[] {
+          json.fromJson(text, BinaryValues.class),
+          json.fromJson(text.getBytes(StandardCharsets.UTF_8), BinaryValues.class)
+        }) {
+      assertEquals(value.label, "汉汉");
+      assertEquals(value.bytes, new byte[] {1, 2});
+      assertEquals(value.list.get(0), new byte[] {3});
+      assertEquals(value.list.get(1), new byte[] {4, 5});
+      assertEquals(value.map.get("a"), new byte[] {6});
+      assertEquals(value.map.get("b"), new byte[] {7, 8});
+      assertEquals(value.nested, new byte[][] {{9}, {10, 11}});
+    }
+    for (String input : new String[] {"[0,null]", "[0,128]", "[-129]", "[a,0]"}) {
+      assertThrows(ForyJsonException.class, () -> json.fromJson(input, byte[].class));
+      assertThrows(
+          ForyJsonException.class,
+          () -> json.fromJson(input.getBytes(StandardCharsets.UTF_8), byte[].class));
+      assertEquals(json.fromJson("[1,2]", byte[].class), new byte[] {1, 2});
+    }
+  }
+
   @Test
   public void conflictingFormats() {
     assertThrows(ForyJsonException.class, () -> newJson().toJson(new ConflictingFormat()));
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 8bf51f717..b039d26ff 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
@@ -325,6 +325,31 @@ public class JsonContainerTest extends ForyJsonTestModels {
     assertThrows(ForyJsonException.class, () -> uint16.readUtf16(newUtf16Reader("[4294967295]")));
   }
 
+  @Test
+  public void byteArrayTokens() {
+    JsonValueCodec<byte[]> codec =
+        ArrayCodec.createUnsignedPrimitive(byte[].class, Types.UINT8_ARRAY, false);
+    for (int size : new int[] {0, 1, 63, 64, 65, 257}) {
+      byte[] values = new byte[size];
+      StringBuilder expected = new StringBuilder("[[");
+      for (int i = 0; i < size; i++) {
+        values[i] = (byte) i;
+        if (i != 0) {
+          expected.append(',');
+        }
+        expected.append(i & 255);
+      }
+      expected.append("],\"tail\"]");
+      Utf8JsonWriter writer = newUtf8Writer(new byte[1]);
+      writer.writeArrayStart();
+      codec.writeUtf8(writer, values);
+      writer.writeComma(1);
+      writer.writeString("tail");
+      writer.writeArrayEnd();
+      assertEquals(new String(writer.toJsonBytes(), StandardCharsets.UTF_8), expected.toString());
+    }
+  }
+
   @Test
   public void readTypeRefList() {
     ForyJson json = newJson();
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 aca9b4a7e..a79a9dfe1 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
@@ -477,6 +477,36 @@ public class JsonScalarTest extends ForyJsonTestModels {
         new String(json.toJsonBytes(new NaturalValues()), StandardCharsets.UTF_8), expected);
   }
 
+  @Test
+  public void writeBase16() {
+    byte[] value = {0, 1, 15, 16, 127, -128, -1};
+    String expected = "[\"汉\",\"00010f107f80ff\",0]";
+    for (int capacity : new int[] {1, 16, 31, 64}) {
+      StringJsonWriter string = newStringWriter(new byte[capacity]);
+      string.writeArrayStart();
+      string.writeString("汉");
+      string.writeComma(1);
+      string.writeBase16(value);
+      string.writeComma(2);
+      string.writeInt(0);
+      string.writeArrayEnd();
+      assertEquals(string.toJson(), expected);
+      string.reset();
+      string.writeBase16(value);
+      assertEquals(string.toJson(), "\"00010f107f80ff\"");
+
+      Utf8JsonWriter utf8 = newUtf8Writer(new byte[capacity]);
+      utf8.writeArrayStart();
+      utf8.writeString("汉");
+      utf8.writeComma(1);
+      utf8.writeBase16(value);
+      utf8.writeComma(2);
+      utf8.writeInt(0);
+      utf8.writeArrayEnd();
+      assertEquals(new String(utf8.toJsonBytes(), StandardCharsets.UTF_8), expected);
+    }
+  }
+
   @Test
   public void writeBase64() {
     ForyJson json = newJson();
diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonTestSupport.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonTestSupport.java
index aa08a0989..f204fd93d 100644
--- a/java/fory-json/src/test/java/org/apache/fory/json/JsonTestSupport.java
+++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonTestSupport.java
@@ -26,6 +26,7 @@ import java.lang.reflect.Field;
 import java.lang.reflect.Method;
 import java.util.Collections;
 import java.util.Map;
+import org.apache.fory.json.annotation.JsonByteArray;
 import org.apache.fory.json.annotation.JsonProperty.Include;
 import org.apache.fory.json.codec.JsonValueCodec;
 import org.apache.fory.json.reader.Latin1JsonReader;
@@ -45,6 +46,7 @@ final class JsonTestSupport {
       new JsonConfig(
           Include.NON_NULL,
           false,
+          JsonByteArray.Format.BASE64,
           false,
           false,
           true,
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 2a1b03c41..cc55baa3a 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
@@ -1151,9 +1151,11 @@ internal class KspModelBuilder(
         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
+          when (format.simpleName.asString()) {
+            "ARRAY" -> "org.apache.fory.json.codec.ArrayCodec\$SignedByteArrayCodec"
+            "BASE16" -> "org.apache.fory.json.codec.Base16ByteArrayCodec"
+            else -> BASE64_CODEC
+          }
       }
       JSON_SUB_TYPES -> collectSubtypeTypes(annotation, result.types)
       else ->
diff --git a/kotlin/fory-json-kotlin/src/test/kotlin/org/apache/fory/json/kotlin/KotlinBuiltInCodecsTest.kt b/kotlin/fory-json-kotlin/src/test/kotlin/org/apache/fory/json/kotlin/KotlinBuiltInCodecsTest.kt
index 256be3bc1..4eefe7ccc 100644
--- a/kotlin/fory-json-kotlin/src/test/kotlin/org/apache/fory/json/kotlin/KotlinBuiltInCodecsTest.kt
+++ b/kotlin/fory-json-kotlin/src/test/kotlin/org/apache/fory/json/kotlin/KotlinBuiltInCodecsTest.kt
@@ -21,6 +21,7 @@ package org.apache.fory.json.kotlin
 
 import java.lang.reflect.Modifier
 import kotlin.test.Test
+import kotlin.test.assertContentEquals
 import kotlin.test.assertEquals
 import kotlin.test.assertFailsWith
 import kotlin.test.assertFalse
@@ -35,6 +36,7 @@ import kotlin.time.TimedValue
 import kotlin.uuid.ExperimentalUuidApi
 import kotlin.uuid.Uuid
 import org.apache.fory.json.ForyJsonException
+import org.apache.fory.json.annotation.JsonByteArray
 import org.apache.fory.json.codec.DirectUnboxedValueCodec
 import org.apache.fory.json.reader.JsonReader
 import org.apache.fory.json.writer.JsonWriter
@@ -86,6 +88,57 @@ class KotlinBuiltInCodecsTest {
 
   private val fory = ForyJsonKotlin.builder().withAsyncCompilation(false).build()
 
+  data class BinaryFields(
+    val label: String,
+    val bytes: ByteArray,
+    val nested: List<ByteArray>,
+    val unsigned: UByteArray
+  )
+
+  @Test
+  fun byteArrayFormats() {
+    val bytes = byteArrayOf(1, -2, 3)
+    val formats =
+      mapOf(
+        JsonByteArray.Format.BASE64 to "\"Af4D\"",
+        JsonByteArray.Format.BASE16 to "\"01fe03\"",
+        JsonByteArray.Format.ARRAY to "[1,-2,3]"
+      )
+    for (codegen in listOf(false, true)) {
+      for ((format, encoded) in formats) {
+        val json =
+          ForyJsonKotlin.builder()
+            .byteArrayFormat(format)
+            .withCodegen(codegen)
+            .withAsyncCompilation(false)
+            .build()
+        assertEquals(encoded, json.toJson(bytes, jsonTypeRef<ByteArray>()))
+        assertContentEquals(bytes, json.fromJson(encoded, jsonTypeRef<ByteArray>()))
+        assertContentEquals(
+          bytes,
+          json.fromJson(encoded.encodeToByteArray(), jsonTypeRef<ByteArray>())
+        )
+        val value = BinaryFields("汉", bytes, listOf(bytes), ubyteArrayOf(1u, 254u, 3u))
+        val text = json.toJson(value)
+        assertTrue(text.contains("\"bytes\":$encoded"), text)
+        assertTrue(text.contains("\"nested\":[$encoded]"), text)
+        assertTrue(text.contains("\"unsigned\":[1,254,3]"), text)
+        assertEquals(text, json.toJsonBytes(value).decodeToString())
+        for (input in listOf(text, json.toPrettyJson(value))) {
+          for (decoded in
+            listOf(
+              json.fromJson(input, BinaryFields::class.java),
+              json.fromJson(input.encodeToByteArray(), BinaryFields::class.java)
+            )) {
+            assertContentEquals(bytes, decoded.bytes)
+            assertContentEquals(bytes, decoded.nested.single())
+            assertContentEquals(value.unsigned, decoded.unsigned)
+          }
+        }
+      }
+    }
+  }
+
   @Test
   fun products() {
     val fory = ForyJsonKotlin.builder().writeNullFields(true).withAsyncCompilation(false).build()
diff --git a/scala/fory-json-scala/src/test/scala/org/apache/fory/json/scala/ScalaJsonSuite.scala b/scala/fory-json-scala/src/test/scala/org/apache/fory/json/scala/ScalaJsonSuite.scala
index 9b567cc9b..d9a5bcd31 100644
--- a/scala/fory-json-scala/src/test/scala/org/apache/fory/json/scala/ScalaJsonSuite.scala
+++ b/scala/fory-json-scala/src/test/scala/org/apache/fory/json/scala/ScalaJsonSuite.scala
@@ -24,7 +24,7 @@ import java.util.concurrent.TimeUnit
 import java.util.concurrent.atomic.AtomicLong
 
 import org.apache.fory.json.{ForyJson, ForyJsonException, JsonCodecFactory}
-import org.apache.fory.json.annotation.{JsonCodec, JsonFormat, JsonIgnore, JsonInclude, JsonMixin, JsonProperty, JsonRawValue, JsonSubTypes, JsonUnwrapped}
+import org.apache.fory.json.annotation.{JsonByteArray, JsonCodec, JsonFormat, JsonIgnore, JsonInclude, JsonMixin, JsonProperty, JsonRawValue, JsonSubTypes, JsonUnwrapped}
 import org.apache.fory.json.codec.{AbstractJsonValueCodec, MapKeyCodec, ObjectCodec}
 import org.apache.fory.json.reader.JsonReader
 import org.apache.fory.json.resolver.{JsonTypeResolver, UnsupportedJsonTypeException}
@@ -41,6 +41,14 @@ case class Node(value: Int, next: Option[Node])
 
 case class BigIntFields(value: BigInt, values: Vector[BigInt])
 
+case class BinaryFields(
+    label: String,
+    bytes: Array[Byte],
+    nested: List[Array[Byte]],
+    mapped: Map[String, Array[Byte]],
+    optional: Option[Array[Byte]]
+)
+
 case class StringScalarFields(
     @JsonFormat(shape = JsonFormat.Shape.STRING) active: Boolean,
     @JsonFormat(shape = JsonFormat.Shape.STRING) count: Int,
@@ -521,6 +529,43 @@ class ScalaJsonSuite extends AnyFunSuite {
     } finally resolver.unlockJIT()
   }
 
+  test("byte array formats") {
+    val bytes = Array[Byte](1, -2, 3)
+    val formats = Seq(
+      JsonByteArray.Format.BASE64 -> "\"Af4D\"",
+      JsonByteArray.Format.BASE16 -> "\"01fe03\"",
+      JsonByteArray.Format.ARRAY -> "[1,-2,3]"
+    )
+    for (codegen <- Seq(false, true); (format, encoded) <- formats) {
+      val json = ForyJsonScala.builder().byteArrayFormat(format)
+        .withCodegen(codegen).withAsyncCompilation(false).build()
+      val byteType = ScalaTypeRef[Array[Byte]]
+      assert(json.toJson(bytes, byteType) == encoded)
+      assert(json.fromJson(encoded, byteType).sameElements(bytes))
+      assert(json.fromJson(encoded.getBytes(UTF_8), byteType).sameElements(bytes))
+      val value = BinaryFields("汉", bytes, List(bytes), Map("data" -> bytes), Some(bytes))
+      val text = json.toJson(value)
+      assert(text.contains("\"bytes\":" + encoded))
+      assert(text.contains("\"nested\":[" + encoded + "]"))
+      assert(text.contains("\"mapped\":{\"data\":" + encoded + "}"))
+      assert(text.contains("\"optional\":" + encoded))
+      assert(new String(json.toJsonBytes(value), UTF_8) == text)
+      for {
+        input <- Seq(text, json.toPrettyJson(value))
+        decoded <- Seq(
+          json.fromJson(input, classOf[BinaryFields]),
+          json.fromJson(input.getBytes(UTF_8), classOf[BinaryFields])
+        )
+      } {
+        assert(decoded.bytes.sameElements(bytes))
+        assert(decoded.nested.head.sameElements(bytes))
+        assert(decoded.mapped("data").sameElements(bytes))
+        assert(decoded.optional.get.sameElements(bytes))
+      }
+      assertWriterGeneration(json, classOf[BinaryFields], codegen)
+    }
+  }
+
   test("scalar string fields and Mixins") {
     for (codegen <- Seq(false, true)) {
       val json = ForyJsonScala.builder().registerMixin(classOf[ArtifactStateMixin])


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@fory.apache.org
For additional commands, e-mail: commits-help@fory.apache.org