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 d0e92c5ca feat(json): support writing long values as strings (#4008)
d0e92c5ca is described below
commit d0e92c5caf9fe35fd786cd069e9cd6d0529c181b
Author: Shawn Yang <[email protected]>
AuthorDate: Mon Aug 31 14:16:29 2026 +0800
feat(json): support writing long values as strings (#4008)
## Why?
## What does this PR do?
## Related issues
Closes #3997
## AI Contribution Checklist
- [ ] Substantial AI assistance was used in this PR: `yes` / `no`
- [ ] If `yes`, I included a completed [AI Contribution
Checklist](https://github.com/apache/fory/blob/main/AI_POLICY.md#9-contributor-checklist-for-ai-assisted-prs)
in this PR description and the required `AI Usage Disclosure`.
- [ ] If `yes`, my PR description includes the required `ai_review`
summary and screenshot evidence or equivalent persisted links of the
final clean AI review results from both fresh reviewers described in
`AI_POLICY.md`, the Fory-guided reviewer and the independent general
reviewer, on the current PR diff or current HEAD after the latest code
changes.
## Does this PR introduce any user-facing change?
- [ ] Does this PR introduce any public API change?
- [ ] Does this PR introduce any binary protocol compatibility change?
## Benchmark
---
docs/json/kotlin.md | 6 +
docs/json/object-mapping.md | 9 +
docs/json/scala.md | 5 +
.../java/org/apache/fory/json/ForyJsonBuilder.java | 17 ++
.../main/java/org/apache/fory/json/JsonConfig.java | 10 +
.../org/apache/fory/json/codec/ArrayCodec.java | 152 +++++++++-
.../apache/fory/json/codec/CollectionCodec.java | 18 +-
.../java/org/apache/fory/json/codec/MapCodec.java | 18 +-
.../org/apache/fory/json/codec/ScalarCodecs.java | 150 +++++++++-
.../fory/json/codegen/StringWriterCodegen.java | 10 +-
.../fory/json/codegen/Utf8WriterCodegen.java | 20 +-
.../org/apache/fory/json/meta/JsonFieldInfo.java | 64 ++++-
.../org/apache/fory/json/reader/JsonReader.java | 7 +
.../json/resolver/GeneratedCodecKeyBuilder.java | 1 +
.../fory/json/resolver/JsonSharedRegistry.java | 40 ++-
.../fory/json/resolver/JsonTypeResolver.java | 6 +
.../org/apache/fory/json/writer/JsonWriter.java | 6 +
.../apache/fory/json/writer/StringJsonWriter.java | 253 +++++++++++++++++
.../apache/fory/json/writer/Utf8JsonWriter.java | 210 ++++++++++++++
.../apache/fory/json/JsonAsyncCompilationTest.java | 1 +
.../org/apache/fory/json/JsonContainerTest.java | 5 +-
.../org/apache/fory/json/JsonLongAsStringTest.java | 309 +++++++++++++++++++++
.../java/org/apache/fory/json/JsonTestSupport.java | 1 +
.../fory/json/kotlin/KotlinJsonCodecFactory.kt | 6 +-
.../fory/json/kotlin/KotlinUnsignedArrayCodecs.kt | 37 ++-
.../fory/json/kotlin/KotlinUnsignedCodecs.kt | 92 +++++-
.../json/kotlin/KotlinValueClassPrimitiveCodecs.kt | 60 +++-
.../apache/fory/json/kotlin/ForyJsonKotlinTest.kt | 122 ++++++++
.../fory/json/kotlin/KotlinBuiltInCodecsTest.kt | 2 +-
.../apache/fory/json/scala/ScalaJsonSuite.scala | 58 ++++
30 files changed, 1643 insertions(+), 52 deletions(-)
diff --git a/docs/json/kotlin.md b/docs/json/kotlin.md
index 0a6825f3c..4b6bc1822 100644
--- a/docs/json/kotlin.md
+++ b/docs/json/kotlin.md
@@ -87,6 +87,12 @@ val text = json.toJson(Account(7u, "Alice"), accountType)
val decoded = json.fromJson(text, accountType)
```
+Use `ForyJsonKotlin.builder().writeLongAsString(true)` when signed `Long` and
unsigned `ULong`
+values must be emitted as quoted decimal strings. The setting also applies to
their declared
+collection and map values, nullable values, Kotlin value classes backed by
them, `ULongArray`, and
+the Java Long-like wrappers supported by the core JSON runtime. Readers accept
both quoted and
+unquoted integer tokens.
+
`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 e64b7116b..537c84860 100644
--- a/docs/json/object-mapping.md
+++ b/docs/json/object-mapping.md
@@ -215,6 +215,7 @@ original key type. Null map keys are rejected.
| Builder method | Default
| User-visible effect |
| -------------------------------------- |
----------------------------------------- |
---------------------------------------------------------- |
| `writeNullFields(boolean)` | `false`
| Default inclusion of null object properties |
+| `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 |
@@ -225,6 +226,14 @@ original key type. Null map keys are rejected.
| `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 |
+Enable `writeLongAsString(true)` when 64-bit integer values must pass through
JavaScript without
+`Number` precision loss. The setting writes built-in `long`/`Long`,
`AtomicLong`,
+`AtomicLongArray`, and `OptionalLong` values as quoted decimal strings. It
also follows declared
+Long children through arrays, collections, map values, `Optional<Long>`,
`AtomicReference<Long>`,
+and equivalent language-module containers. Readers accept both numeric and
quoted integer tokens
+regardless of this setting. Custom codecs and occurrence-level codec or format
annotations retain
+their own output shape.
+
Concurrency-level and buffer-retention limits must be positive. The
cached-field-name limit
applies independently to each reader; zero disables this cache. It bounds only
cached field names,
not names accepted from the input. The buffer-retention setting does not limit
JSON input or output
diff --git a/docs/json/scala.md b/docs/json/scala.md
index 04b9637f3..f1bdf4a57 100644
--- a/docs/json/scala.md
+++ b/docs/json/scala.md
@@ -41,6 +41,11 @@ val person = json.fromJson(text, classOf[Person])
```
Reuse the resulting `ForyJson` instance. It is immutable and thread-safe after
construction.
+Use `ForyJsonScala.builder().writeLongAsString(true)` to emit Scala `Long`
values, including
+declared collection and map values, `Option[Long]`, `Long`-backed value
classes, and Java Long-like
+wrappers as quoted decimal strings. Readers accept both quoted and unquoted
integer tokens.
+Use `ScalaTypeRef` when a parameterized declaration contains `Long` because
normal JVM signatures
+can erase Scala value-type arguments to `Object`.
## Case classes and annotations
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 301d05b8c..805b64526 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
@@ -50,6 +50,7 @@ import org.apache.fory.platform.GraalvmSupport;
*/
public final class ForyJsonBuilder {
private boolean writeNullFields;
+ private boolean writeLongAsString;
private boolean codegenEnabled = true;
private boolean asyncCompilationEnabled = true;
private boolean propertyDiscoveryEnabled = true;
@@ -80,6 +81,21 @@ public final class ForyJsonBuilder {
return this;
}
+ /**
+ * Writes signed 64-bit integer values owned by Fory's built-in JSON codecs
as quoted decimal
+ * strings. This includes {@code long}/{@link Long}, {@code AtomicLong},
{@code AtomicLongArray},
+ * {@code OptionalLong}, and generic containers whose declared value is
{@code Long}. Disabled by
+ * default.
+ *
+ * <p>This setting also applies to corresponding 64-bit unsigned scalar
bindings installed by a
+ * language module. Exact custom codecs and occurrence-level codec or format
annotations retain
+ * ownership of their complete representation.
+ */
+ public ForyJsonBuilder writeLongAsString(boolean writeLongAsString) {
+ this.writeLongAsString = writeLongAsString;
+ 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
@@ -297,6 +313,7 @@ public final class ForyJsonBuilder {
ModuleInstaller.install(new ArrayList<>(modules), codecRegistry,
mixins);
return new JsonConfig(
writeNullFields,
+ writeLongAsString,
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 a398f6b6f..029c81a58 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
@@ -37,6 +37,7 @@ public final class JsonConfig {
private static final int MAX_CACHED_FIELD_NAMES = 1 << 29;
private final boolean writeNullFields;
+ private final boolean writeLongAsString;
private final boolean codegenEnabled;
private final boolean asyncCompilationEnabled;
private final boolean propertyDiscoveryEnabled;
@@ -56,6 +57,7 @@ public final class JsonConfig {
JsonConfig(
boolean writeNullFields,
+ boolean writeLongAsString,
boolean codegenEnabled,
boolean asyncCompilationEnabled,
boolean propertyDiscoveryEnabled,
@@ -72,6 +74,7 @@ public final class JsonConfig {
List<String> factoryIdentities,
JsonTypeChecker typeChecker) {
this.writeNullFields = writeNullFields;
+ this.writeLongAsString = writeLongAsString;
this.codegenEnabled = codegenEnabled;
this.asyncCompilationEnabled = asyncCompilationEnabled;
this.propertyDiscoveryEnabled = propertyDiscoveryEnabled;
@@ -97,6 +100,13 @@ public final class JsonConfig {
return writeNullFields;
}
+ /**
+ * Returns whether built-in signed and module-provided unsigned 64-bit
values write as strings.
+ */
+ public boolean writeLongAsString() {
+ return writeLongAsString;
+ }
+
public boolean codegenEnabled() {
return codegenEnabled;
}
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 cf3499eec..86c2a0244 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
@@ -80,6 +80,9 @@ public abstract class ArrayCodec<T> implements
JsonValueCodec<T> {
return bind(IntArrayCodec.INSTANCE);
} else if (componentType == long.class && componentCodec ==
ScalarCodecs.LongCodec.PRIMITIVE) {
return bind(LongArrayCodec.INSTANCE);
+ } else if (componentType == long.class
+ && componentCodec == ScalarCodecs.LongAsStringCodec.PRIMITIVE) {
+ return bind(LongAsStringArrayCodec.INSTANCE);
} else if (componentType == boolean.class
&& componentCodec == ScalarCodecs.BooleanCodec.PRIMITIVE) {
return bind(BooleanArrayCodec.INSTANCE);
@@ -106,6 +109,12 @@ public abstract class ArrayCodec<T> implements
JsonValueCodec<T> {
componentTypeInfo.rejectsNull()
? BoxedLongArrayCodec.NON_NULL
: BoxedLongArrayCodec.INSTANCE);
+ } else if (componentType == Long.class
+ && componentCodec == ScalarCodecs.LongAsStringCodec.BOXED) {
+ return bind(
+ componentTypeInfo.rejectsNull()
+ ? BoxedLongAsStringArrayCodec.NON_NULL
+ : BoxedLongAsStringArrayCodec.INSTANCE);
} else if (componentType == Boolean.class
&& componentCodec == ScalarCodecs.BooleanCodec.BOXED) {
return bind(
@@ -152,7 +161,8 @@ public abstract class ArrayCodec<T> implements
JsonValueCodec<T> {
/** Returns the exact unsigned primitive-array specialization for one
semantic array id. */
@Internal
- public static <T> ArrayCodec<T> createUnsignedPrimitive(Class<T> arrayType,
int typeId) {
+ public static <T> ArrayCodec<T> createUnsignedPrimitive(
+ Class<T> arrayType, int typeId, boolean writeLongAsString) {
if (arrayType == byte[].class && typeId == Types.UINT8_ARRAY) {
return bind(ByteArrayCodec.UNSIGNED);
}
@@ -163,7 +173,10 @@ public abstract class ArrayCodec<T> implements
JsonValueCodec<T> {
return bind(IntArrayCodec.UNSIGNED);
}
if (arrayType == long[].class && typeId == Types.UINT64_ARRAY) {
- return bind(LongArrayCodec.UNSIGNED);
+ return bind(
+ writeLongAsString
+ ? UnsignedLongAsStringArrayCodec.INSTANCE
+ : UnsignedLongArrayCodec.INSTANCE);
}
throw new ForyJsonException(
"Unsigned JSON array semantic id " + typeId + " does not match " +
arrayType.getName());
@@ -389,11 +402,10 @@ public abstract class ArrayCodec<T> implements
JsonValueCodec<T> {
// Keep the signed codec as the exact final loop owner. Routing signed long
arrays through a
// semantic element hook makes C2 inline the root and array loop into one
unstable graph; the
// unsigned codec below owns separate primitive operations so neither warmed
path branches.
- public static final class LongArrayCodec extends ArrayCodec<long[]> {
+ public static class LongArrayCodec extends ArrayCodec<long[]> {
private static final LongArrayCodec INSTANCE = new LongArrayCodec();
- private static final ArrayCodec<long[]> UNSIGNED = new
UnsignedLongArrayCodec();
- private LongArrayCodec() {
+ protected LongArrayCodec() {
super(long.class);
}
@@ -734,7 +746,44 @@ public abstract class ArrayCodec<T> implements
JsonValueCodec<T> {
}
}
- private static final class UnsignedLongArrayCodec extends ArrayCodec<long[]>
{
+ /** Exact signed long-array specialization which writes each element as a
JSON string. */
+ public static final class LongAsStringArrayCodec extends LongArrayCodec {
+ private static final LongAsStringArrayCodec INSTANCE = new
LongAsStringArrayCodec();
+
+ private LongAsStringArrayCodec() {}
+
+ @Override
+ public void writeString(StringJsonWriter writer, long[] value) {
+ if (value == null) {
+ writer.writeNull();
+ return;
+ }
+ writer.writeArrayStart();
+ for (int i = 0; i < value.length; i++) {
+ writer.writeComma(i);
+ writer.writeLongAsString(value[i]);
+ }
+ writer.writeArrayEnd();
+ }
+
+ @Override
+ public void writeUtf8(Utf8JsonWriter writer, long[] value) {
+ if (value == null) {
+ writer.writeNull();
+ return;
+ }
+ writer.writeArrayStart();
+ for (int i = 0; i < value.length; i++) {
+ writer.writeComma(i);
+ writer.writeLongAsString(value[i]);
+ }
+ writer.writeArrayEnd();
+ }
+ }
+
+ private static class UnsignedLongArrayCodec extends ArrayCodec<long[]> {
+ private static final UnsignedLongArrayCodec INSTANCE = new
UnsignedLongArrayCodec();
+
private UnsignedLongArrayCodec() {
super(long.class);
}
@@ -867,6 +916,41 @@ public abstract class ArrayCodec<T> implements
JsonValueCodec<T> {
}
}
+ private static final class UnsignedLongAsStringArrayCodec extends
UnsignedLongArrayCodec {
+ private static final UnsignedLongAsStringArrayCodec INSTANCE =
+ new UnsignedLongAsStringArrayCodec();
+
+ private UnsignedLongAsStringArrayCodec() {}
+
+ @Override
+ public void writeString(StringJsonWriter writer, long[] value) {
+ if (value == null) {
+ writer.writeNull();
+ return;
+ }
+ writer.writeArrayStart();
+ for (int i = 0; i < value.length; i++) {
+ writer.writeComma(i);
+ writer.writeUnsignedLongAsString(value[i]);
+ }
+ writer.writeArrayEnd();
+ }
+
+ @Override
+ public void writeUtf8(Utf8JsonWriter writer, long[] value) {
+ if (value == null) {
+ writer.writeNull();
+ return;
+ }
+ writer.writeArrayStart();
+ for (int i = 0; i < value.length; i++) {
+ writer.writeComma(i);
+ writer.writeUnsignedLongAsString(value[i]);
+ }
+ writer.writeArrayEnd();
+ }
+ }
+
public static final class BooleanArrayCodec extends ArrayCodec<boolean[]> {
private static final BooleanArrayCodec INSTANCE = new BooleanArrayCodec();
private static final int ELEMENT_BYTES = 1;
@@ -2662,12 +2746,12 @@ public abstract class ArrayCodec<T> implements
JsonValueCodec<T> {
}
}
- public static final class BoxedLongArrayCodec extends ArrayCodec<Long[]> {
+ public static class BoxedLongArrayCodec extends ArrayCodec<Long[]> {
private static final BoxedLongArrayCodec INSTANCE = new
BoxedLongArrayCodec(false);
private static final BoxedLongArrayCodec NON_NULL = new
BoxedLongArrayCodec(true);
private final boolean rejectsNull;
- private BoxedLongArrayCodec(boolean rejectsNull) {
+ protected BoxedLongArrayCodec(boolean rejectsNull) {
super(Long.class);
this.rejectsNull = rejectsNull;
}
@@ -2797,6 +2881,58 @@ public abstract class ArrayCodec<T> implements
JsonValueCodec<T> {
}
}
+ private static final class BoxedLongAsStringArrayCodec extends
BoxedLongArrayCodec {
+ private static final BoxedLongAsStringArrayCodec INSTANCE =
+ new BoxedLongAsStringArrayCodec(false);
+ private static final BoxedLongAsStringArrayCodec NON_NULL =
+ new BoxedLongAsStringArrayCodec(true);
+
+ private final boolean rejectsNull;
+
+ private BoxedLongAsStringArrayCodec(boolean rejectsNull) {
+ super(rejectsNull);
+ this.rejectsNull = rejectsNull;
+ }
+
+ @Override
+ public void writeString(StringJsonWriter writer, Long[] value) {
+ if (value == null) {
+ writer.writeNull();
+ return;
+ }
+ writer.writeArrayStart();
+ for (int i = 0; i < value.length; i++) {
+ writer.writeComma(i);
+ Long element = value[i];
+ if (element == null) {
+ writeReferenceNull(writer, rejectsNull);
+ } else {
+ writer.writeLongAsString(element);
+ }
+ }
+ writer.writeArrayEnd();
+ }
+
+ @Override
+ public void writeUtf8(Utf8JsonWriter writer, Long[] value) {
+ if (value == null) {
+ writer.writeNull();
+ return;
+ }
+ writer.writeArrayStart();
+ for (int i = 0; i < value.length; i++) {
+ writer.writeComma(i);
+ Long element = value[i];
+ if (element == null) {
+ writeReferenceNull(writer, rejectsNull);
+ } else {
+ writer.writeLongAsString(element);
+ }
+ }
+ writer.writeArrayEnd();
+ }
+ }
+
public static final class BoxedBooleanArrayCodec extends
ArrayCodec<Boolean[]> {
private static final BoxedBooleanArrayCodec INSTANCE = new
BoxedBooleanArrayCodec(false);
private static final BoxedBooleanArrayCodec NON_NULL = new
BoxedBooleanArrayCodec(true);
diff --git
a/java/fory-json/src/main/java/org/apache/fory/json/codec/CollectionCodec.java
b/java/fory-json/src/main/java/org/apache/fory/json/codec/CollectionCodec.java
index 6c077b28c..840f31566 100644
---
a/java/fory-json/src/main/java/org/apache/fory/json/codec/CollectionCodec.java
+++
b/java/fory-json/src/main/java/org/apache/fory/json/codec/CollectionCodec.java
@@ -153,6 +153,9 @@ public abstract class CollectionCodec<T extends
Collection<?>> implements JsonVa
if (elementCodec == ScalarCodecs.LongCodec.BOXED) {
return new LongCollectionCodec(factory, elementTypeInfo);
}
+ if (elementCodec == ScalarCodecs.LongAsStringCodec.BOXED) {
+ return new LongAsStringCollectionCodec(factory, elementTypeInfo);
+ }
if (elementCodec == ScalarCodecs.ShortCodec.BOXED) {
return new ShortCollectionCodec(factory, elementTypeInfo);
}
@@ -1942,8 +1945,8 @@ public abstract class CollectionCodec<T extends
Collection<?>> implements JsonVa
}
}
- public static final class LongCollectionCodec extends NumberCollectionCodec {
- private LongCollectionCodec(CollectionFactory factory, JsonTypeInfo
elementTypeInfo) {
+ public static class LongCollectionCodec extends NumberCollectionCodec {
+ protected LongCollectionCodec(CollectionFactory factory, JsonTypeInfo
elementTypeInfo) {
super(factory, elementTypeInfo);
}
@@ -1968,6 +1971,17 @@ public abstract class CollectionCodec<T extends
Collection<?>> implements JsonVa
}
}
+ private static final class LongAsStringCollectionCodec extends
LongCollectionCodec {
+ private LongAsStringCollectionCodec(CollectionFactory factory,
JsonTypeInfo elementTypeInfo) {
+ super(factory, elementTypeInfo);
+ }
+
+ @Override
+ void writeNumber(JsonWriter writer, Object value) {
+ writer.writeLongAsString((long) value);
+ }
+ }
+
public static final class ShortCollectionCodec extends NumberCollectionCodec
{
private ShortCollectionCodec(CollectionFactory factory, JsonTypeInfo
elementTypeInfo) {
super(factory, elementTypeInfo);
diff --git
a/java/fory-json/src/main/java/org/apache/fory/json/codec/MapCodec.java
b/java/fory-json/src/main/java/org/apache/fory/json/codec/MapCodec.java
index 355f374f9..bd2231aed 100644
--- a/java/fory-json/src/main/java/org/apache/fory/json/codec/MapCodec.java
+++ b/java/fory-json/src/main/java/org/apache/fory/json/codec/MapCodec.java
@@ -189,6 +189,9 @@ public abstract class MapCodec<T extends Map<?, ?>>
implements JsonValueCodec<T>
if (valueCodec == ScalarCodecs.LongCodec.BOXED) {
return new StringLongMapCodec(factory, valueTypeInfo);
}
+ if (valueCodec == ScalarCodecs.LongAsStringCodec.BOXED) {
+ return new StringLongAsStringMapCodec(factory, valueTypeInfo);
+ }
if (valueCodec == ScalarCodecs.ShortCodec.BOXED) {
return new StringShortMapCodec(factory, valueTypeInfo);
}
@@ -1037,8 +1040,8 @@ public abstract class MapCodec<T extends Map<?, ?>>
implements JsonValueCodec<T>
}
}
- public static final class StringLongMapCodec extends StringNumberMapCodec {
- private StringLongMapCodec(MapFactory factory, JsonTypeInfo valueTypeInfo)
{
+ public static class StringLongMapCodec extends StringNumberMapCodec {
+ protected StringLongMapCodec(MapFactory factory, JsonTypeInfo
valueTypeInfo) {
super(factory, valueTypeInfo);
}
@@ -1063,6 +1066,17 @@ public abstract class MapCodec<T extends Map<?, ?>>
implements JsonValueCodec<T>
}
}
+ private static final class StringLongAsStringMapCodec extends
StringLongMapCodec {
+ private StringLongAsStringMapCodec(MapFactory factory, JsonTypeInfo
valueTypeInfo) {
+ super(factory, valueTypeInfo);
+ }
+
+ @Override
+ void writeNumber(JsonWriter writer, Object value) {
+ writer.writeLongAsString((long) value);
+ }
+ }
+
public static final class StringShortMapCodec extends StringNumberMapCodec {
private StringShortMapCodec(MapFactory factory, JsonTypeInfo
valueTypeInfo) {
super(factory, valueTypeInfo);
diff --git
a/java/fory-json/src/main/java/org/apache/fory/json/codec/ScalarCodecs.java
b/java/fory-json/src/main/java/org/apache/fory/json/codec/ScalarCodecs.java
index 2e33d4ecd..71ea52dae 100644
--- a/java/fory-json/src/main/java/org/apache/fory/json/codec/ScalarCodecs.java
+++ b/java/fory-json/src/main/java/org/apache/fory/json/codec/ScalarCodecs.java
@@ -412,12 +412,12 @@ public final class ScalarCodecs {
}
}
- public static final class LongCodec implements JsonValueCodec<Long> {
+ public static class LongCodec implements JsonValueCodec<Long> {
public static final LongCodec PRIMITIVE = new LongCodec(true);
public static final LongCodec BOXED = new LongCodec(false);
private final boolean primitive;
- private LongCodec(boolean primitive) {
+ protected LongCodec(boolean primitive) {
this.primitive = primitive;
}
@@ -464,6 +464,34 @@ public final class ScalarCodecs {
}
}
+ /** Built-in signed Long binding which writes decimal digits as a JSON
string. */
+ public static final class LongAsStringCodec extends LongCodec {
+ public static final LongAsStringCodec PRIMITIVE = new
LongAsStringCodec(true);
+ public static final LongAsStringCodec BOXED = new LongAsStringCodec(false);
+
+ private LongAsStringCodec(boolean primitive) {
+ super(primitive);
+ }
+
+ @Override
+ public void writeString(StringJsonWriter writer, Long value) {
+ if (value == null) {
+ writer.writeNull();
+ } else {
+ writer.writeLongAsString(value);
+ }
+ }
+
+ @Override
+ public void writeUtf8(Utf8JsonWriter writer, Long value) {
+ if (value == null) {
+ writer.writeNull();
+ } else {
+ writer.writeLongAsString(value);
+ }
+ }
+ }
+
public static final class ShortCodec implements JsonValueCodec<Short> {
public static final ShortCodec PRIMITIVE = new ShortCodec(true);
public static final ShortCodec BOXED = new ShortCodec(false);
@@ -2631,9 +2659,11 @@ public final class ScalarCodecs {
}
}
- public static final class AtomicLongCodec implements
JsonValueCodec<AtomicLong> {
+ public static class AtomicLongCodec implements JsonValueCodec<AtomicLong> {
public static final AtomicLongCodec INSTANCE = new AtomicLongCodec();
+ protected AtomicLongCodec() {}
+
@Override
public void writeString(StringJsonWriter writer, AtomicLong value) {
if (value == null) {
@@ -2668,6 +2698,31 @@ public final class ScalarCodecs {
}
}
+ /** Built-in AtomicLong binding which writes its Long value as a JSON
string. */
+ public static final class AtomicLongAsStringCodec extends AtomicLongCodec {
+ public static final AtomicLongAsStringCodec INSTANCE = new
AtomicLongAsStringCodec();
+
+ private AtomicLongAsStringCodec() {}
+
+ @Override
+ public void writeString(StringJsonWriter writer, AtomicLong value) {
+ if (value == null) {
+ writer.writeNull();
+ } else {
+ writer.writeLongAsString(value.get());
+ }
+ }
+
+ @Override
+ public void writeUtf8(Utf8JsonWriter writer, AtomicLong value) {
+ if (value == null) {
+ writer.writeNull();
+ } else {
+ writer.writeLongAsString(value.get());
+ }
+ }
+ }
+
public static class AtomicReferenceCodec implements
JsonValueCodec<AtomicReference<?>> {
static final int SHALLOW_BYTES =
GraphMemoryEstimates.shallowObjectBytes(AtomicReference.class);
@@ -2970,9 +3025,11 @@ public final class ScalarCodecs {
}
}
- public static final class AtomicLongArrayCodec implements
JsonValueCodec<AtomicLongArray> {
+ public static class AtomicLongArrayCodec implements
JsonValueCodec<AtomicLongArray> {
public static final AtomicLongArrayCodec INSTANCE = new
AtomicLongArrayCodec();
+ protected AtomicLongArrayCodec() {}
+
@Override
public void writeString(StringJsonWriter writer, AtomicLongArray value) {
if (value == null) {
@@ -3088,6 +3145,43 @@ public final class ScalarCodecs {
}
}
+ /** Built-in AtomicLongArray binding which writes each Long value as a JSON
string. */
+ public static final class AtomicLongArrayAsStringCodec extends
AtomicLongArrayCodec {
+ public static final AtomicLongArrayAsStringCodec INSTANCE = new
AtomicLongArrayAsStringCodec();
+
+ private AtomicLongArrayAsStringCodec() {}
+
+ @Override
+ public void writeString(StringJsonWriter writer, AtomicLongArray value) {
+ if (value == null) {
+ writer.writeNull();
+ return;
+ }
+ AtomicLongArray array = value;
+ writer.writeArrayStart();
+ for (int i = 0, length = array.length(); i < length; i++) {
+ writer.writeComma(i);
+ writer.writeLongAsString(array.get(i));
+ }
+ writer.writeArrayEnd();
+ }
+
+ @Override
+ public void writeUtf8(Utf8JsonWriter writer, AtomicLongArray value) {
+ if (value == null) {
+ writer.writeNull();
+ return;
+ }
+ AtomicLongArray array = value;
+ writer.writeArrayStart();
+ for (int i = 0, length = array.length(); i < length; i++) {
+ writer.writeComma(i);
+ writer.writeLongAsString(array.get(i));
+ }
+ writer.writeArrayEnd();
+ }
+ }
+
public static class AtomicReferenceArrayCodec implements
JsonValueCodec<AtomicReferenceArray<?>> {
private static final int SHALLOW_BYTES =
GraphMemoryEstimates.shallowObjectBytes(AtomicReferenceArray.class);
@@ -3486,14 +3580,14 @@ public final class ScalarCodecs {
}
}
- public static final class OptionalLongCodec
+ public static class OptionalLongCodec
implements JsonValueCodec<OptionalLong>, TransparentNullCodec {
public static final OptionalLongCodec INSTANCE = new
OptionalLongCodec(false);
@Internal public static final OptionalLongCodec NON_NULL = new
OptionalLongCodec(true);
- private final boolean requireOwner;
+ protected final boolean requireOwner;
- private OptionalLongCodec(boolean requireOwner) {
+ protected OptionalLongCodec(boolean requireOwner) {
this.requireOwner = requireOwner;
}
@@ -3543,6 +3637,48 @@ public final class ScalarCodecs {
}
}
+ /** Built-in OptionalLong binding which writes its present Long value as a
JSON string. */
+ public static final class OptionalLongAsStringCodec extends
OptionalLongCodec {
+ public static final OptionalLongAsStringCodec INSTANCE = new
OptionalLongAsStringCodec(false);
+
+ @Internal
+ public static final OptionalLongAsStringCodec NON_NULL = new
OptionalLongAsStringCodec(true);
+
+ private OptionalLongAsStringCodec(boolean requireOwner) {
+ super(requireOwner);
+ }
+
+ @Override
+ public void writeString(StringJsonWriter writer, OptionalLong value) {
+ if (value == null) {
+ requireOptionalOwner(requireOwner);
+ writer.writeNull();
+ return;
+ }
+ OptionalLong optional = value;
+ if (optional.isPresent()) {
+ writer.writeLongAsString(optional.getAsLong());
+ } else {
+ writer.writeNull();
+ }
+ }
+
+ @Override
+ public void writeUtf8(Utf8JsonWriter writer, OptionalLong value) {
+ if (value == null) {
+ requireOptionalOwner(requireOwner);
+ writer.writeNull();
+ return;
+ }
+ OptionalLong optional = value;
+ if (optional.isPresent()) {
+ writer.writeLongAsString(optional.getAsLong());
+ } else {
+ writer.writeNull();
+ }
+ }
+ }
+
public static final class OptionalDoubleCodec
implements JsonValueCodec<OptionalDouble>, TransparentNullCodec {
public static final OptionalDoubleCodec INSTANCE = new
OptionalDoubleCodec(false);
diff --git
a/java/fory-json/src/main/java/org/apache/fory/json/codegen/StringWriterCodegen.java
b/java/fory-json/src/main/java/org/apache/fory/json/codegen/StringWriterCodegen.java
index b898fc847..3bb77ba1e 100644
---
a/java/fory-json/src/main/java/org/apache/fory/json/codegen/StringWriterCodegen.java
+++
b/java/fory-json/src/main/java/org/apache/fory/json/codegen/StringWriterCodegen.java
@@ -183,7 +183,10 @@ final class StringWriterCodegen extends JsonWriterCodegen {
method = "writeObjectStartWithIntField";
break;
case LONG:
- method = "writeObjectStartWithLongField";
+ method =
+ property.writesLongAsString()
+ ? "writeObjectStartWithLongAsStringField"
+ : "writeObjectStartWithLongField";
break;
default:
throw new ForyJsonException(
@@ -212,7 +215,10 @@ final class StringWriterCodegen extends JsonWriterCodegen {
boolean commaKnown,
Expression index,
Expression writer) {
- String method = longValue ? "writeLongField" : "writeIntField";
+ String method =
+ longValue
+ ? property.writesLongAsString() ? "writeLongAsStringField" :
"writeLongField"
+ : "writeIntField";
if (commaKnown) {
if (canPackUtf16Prefix(property, true)) {
return new Expression.Invoke(
diff --git
a/java/fory-json/src/main/java/org/apache/fory/json/codegen/Utf8WriterCodegen.java
b/java/fory-json/src/main/java/org/apache/fory/json/codegen/Utf8WriterCodegen.java
index 7b0b8ba1a..1c49ba284 100644
---
a/java/fory-json/src/main/java/org/apache/fory/json/codegen/Utf8WriterCodegen.java
+++
b/java/fory-json/src/main/java/org/apache/fory/json/codegen/Utf8WriterCodegen.java
@@ -187,7 +187,10 @@ final class Utf8WriterCodegen extends JsonWriterCodegen {
method = "writeObjectStartWithIntField";
break;
case LONG:
- method = "writeObjectStartWithLongField";
+ method =
+ property.writesLongAsString()
+ ? "writeObjectStartWithLongAsStringField"
+ : "writeObjectStartWithLongField";
break;
default:
throw new ForyJsonException(
@@ -226,13 +229,21 @@ final class Utf8WriterCodegen extends JsonWriterCodegen {
boolean commaKnown,
Expression index,
Expression writer) {
- String method = longValue ? "writeLongField" : "writeIntField";
+ String method =
+ longValue
+ ? property.writesLongAsString() ? "writeLongAsStringField" :
"writeLongField"
+ : "writeIntField";
if (commaKnown) {
if (canPackPrefix(property, true)) {
if (inlineSchemaWrites) {
return new Expression.ListExpression(
directPackedPrefix(property, id),
- new Expression.Invoke(writer, longValue ? "writeLong" :
"writeInt", value));
+ new Expression.Invoke(
+ writer,
+ longValue
+ ? property.writesLongAsString() ? "writeLongAsString" :
"writeLong"
+ : "writeInt",
+ value));
}
return new Expression.Invoke(writer, method,
packedPrefixArgs(property, true, value));
}
@@ -368,6 +379,9 @@ final class Utf8WriterCodegen extends JsonWriterCodegen {
if (rawType == long[].class && codecType ==
ArrayCodec.LongArrayCodec.class) {
return new Expression.Invoke(writer, "writeLongArray", value);
}
+ if (rawType == long[].class && codecType ==
ArrayCodec.LongAsStringArrayCodec.class) {
+ return new Expression.Invoke(writer, "writeLongArrayAsString", value);
+ }
return null;
}
diff --git
a/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonFieldInfo.java
b/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonFieldInfo.java
index 7ecbb8127..5dac22025 100644
--- a/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonFieldInfo.java
+++ b/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonFieldInfo.java
@@ -32,6 +32,7 @@ import org.apache.fory.json.annotation.JsonFormat;
import org.apache.fory.json.codec.CodecUtils;
import org.apache.fory.json.codec.DirectUnboxedValueCodec;
import org.apache.fory.json.codec.JsonValueCodec;
+import org.apache.fory.json.codec.ScalarCodecs;
import org.apache.fory.json.codec.TransparentUnboxedValueCodec;
import org.apache.fory.json.codec.UnboxedValueCodec;
import org.apache.fory.json.reader.JsonReader;
@@ -81,6 +82,7 @@ public final class JsonFieldInfo {
private static final int KIND_RAW_STRING = 16;
private static final int KIND_NULL = 17;
private static final int KIND_UNBOXED = 18;
+ private static final int KIND_LONG_AS_STRING = 19;
private static final int WRITE_NULL_MASK = Integer.MIN_VALUE;
private static final int REQUIRE_NON_NULL_MASK = 1 << 30;
private static final int READ_INDEX_MASK = REQUIRE_NON_NULL_MASK - 1;
@@ -463,6 +465,11 @@ public final class JsonFieldInfo {
return writeKindId == KIND_RAW_STRING;
}
+ /** Returns whether the resolved built-in Long binding writes quoted decimal
digits. */
+ public boolean writesLongAsString() {
+ return writeKind == JsonFieldKind.LONG && isLongAsString(writeTypeInfo);
+ }
+
public JsonFieldAccessor writeAccessor() {
return writeAccessor;
}
@@ -579,7 +586,13 @@ public final class JsonFieldInfo {
}
if (!rawString && writeRawType != void.class) {
writeKind = writeTypeInfo.kind();
- writeKindId = writeUnboxedValueCodec == null ? kindId(writeKind) :
KIND_UNBOXED;
+ if (writeUnboxedValueCodec != null) {
+ writeKindId = KIND_UNBOXED;
+ } else if (writeKind == JsonFieldKind.LONG &&
isLongAsString(writeTypeInfo)) {
+ writeKindId = KIND_LONG_AS_STRING;
+ } else {
+ writeKindId = kindId(writeKind);
+ }
}
if (writeUnboxedValueCodec != null
&& !writeOccurrenceTypeInfo.nullable()
@@ -610,6 +623,12 @@ public final class JsonFieldInfo {
}
}
+ private static boolean isLongAsString(JsonTypeInfo typeInfo) {
+ Object codec = typeInfo.stringWriter();
+ return codec == ScalarCodecs.LongAsStringCodec.PRIMITIVE
+ || codec == ScalarCodecs.LongAsStringCodec.BOXED;
+ }
+
private UnboxedValueCodec requireUnboxed(
JsonTypeInfo canonical, Class<?> carrier, String direction) {
UnboxedValueCodec operation = canonical.unboxedValueCodec();
@@ -1108,6 +1127,13 @@ public final class JsonFieldInfo {
writer.writeLongField(
stringNamePrefix, stringCommaNamePrefix, index,
writeAccessor.getLong(object));
return true;
+ case KIND_LONG_AS_STRING:
+ if (!writeRawType.isPrimitive()) {
+ return writeStringLongAsString(writer, object, index);
+ }
+ writer.writeLongAsStringField(
+ stringNamePrefix, stringCommaNamePrefix, index,
writeAccessor.getLong(object));
+ return true;
case KIND_FLOAT:
if (!writeRawType.isPrimitive()) {
return writeStringScalar(writer, object, index);
@@ -1190,6 +1216,13 @@ public final class JsonFieldInfo {
writer.writeLongField(
utf8NamePrefix, utf8CommaNamePrefix, index,
writeAccessor.getLong(object));
return true;
+ case KIND_LONG_AS_STRING:
+ if (!writeRawType.isPrimitive()) {
+ return writeUtf8LongAsString(writer, object, index);
+ }
+ writer.writeLongAsStringField(
+ utf8NamePrefix, utf8CommaNamePrefix, index,
writeAccessor.getLong(object));
+ return true;
case KIND_FLOAT:
if (!writeRawType.isPrimitive()) {
return writeUtf8Scalar(writer, object, index);
@@ -1292,6 +1325,21 @@ public final class JsonFieldInfo {
}
}
+ private boolean writeStringLongAsString(StringJsonWriter writer, Object
object, int index) {
+ Long value = (Long) writeAccessor.getObject(object);
+ if (value == null && !writeNull()) {
+ return omitNullValue();
+ }
+ if (value == null) {
+ writer.writeFieldName(this, index);
+ writer.writeNull();
+ } else {
+ writer.writeLongAsStringField(
+ stringNamePrefix, stringCommaNamePrefix, index, value.longValue());
+ }
+ return true;
+ }
+
private boolean writeUtf8Scalar(Utf8JsonWriter writer, Object object, int
index) {
Object value = writeAccessor.getObject(object);
if (value == null && !writeNull()) {
@@ -1328,6 +1376,20 @@ public final class JsonFieldInfo {
}
}
+ private boolean writeUtf8LongAsString(Utf8JsonWriter writer, Object object,
int index) {
+ Long value = (Long) writeAccessor.getObject(object);
+ if (value == null && !writeNull()) {
+ return omitNullValue();
+ }
+ if (value == null) {
+ writer.writeFieldName(this, index);
+ writer.writeNull();
+ } else {
+ writer.writeLongAsStringField(utf8NamePrefix, utf8CommaNamePrefix,
index, value.longValue());
+ }
+ return true;
+ }
+
private boolean writeStringText(StringJsonWriter writer, Object object, int
index) {
String value = (String) writeAccessor.getObject(object);
if (value == null && !writeNull()) {
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 dfd4359f0..1438f0143 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
@@ -892,6 +892,13 @@ public abstract class JsonReader {
/** Reads one canonical unsigned 64-bit JSON integer and returns its raw
bits. */
public final long readUnsignedLong() {
skipWhitespace();
+ if (position < length() && charAt(position) == '"') {
+ beginQuotedScalar();
+ long value = readUnsignedDigits();
+ rejectFractionOrExponent();
+ finishQuotedScalar();
+ return value;
+ }
long value = readUnsignedDigits();
rejectFractionOrExponent();
return value;
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 b1853db49..d6f5f9f52 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
@@ -72,6 +72,7 @@ final class GeneratedCodecKeyBuilder {
JsonSharedRegistry registry = resolver.sharedRegistry();
if (!JsonTypeResolver.readerKind(kind)) {
keyParts.add(registry.writeNullFields());
+ keyParts.add(registry.writeLongAsString());
}
keyParts.add(registry.propertyDiscoveryEnabled());
keyParts.add(registry.propertyNamingStrategy());
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 4c8b53ce4..60fe63ddc 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
@@ -187,6 +187,7 @@ public final class JsonSharedRegistry {
private final boolean propertyDiscoveryEnabled;
private final PropertyNamingStrategy propertyNamingStrategy;
private final boolean writeNullFields;
+ private final boolean writeLongAsString;
private final ClassLoader classLoader;
private final JsonMixinAnnotations mixinAnnotations;
private final IdentityHashMap<Class<?>, JsonSubTypesInfo> subTypesCache;
@@ -243,6 +244,7 @@ public final class JsonSharedRegistry {
this.propertyDiscoveryEnabled = config.propertyDiscoveryEnabled();
propertyNamingStrategy = config.propertyNamingStrategy();
writeNullFields = config.writeNullFields();
+ writeLongAsString = config.writeLongAsString();
classLoader = config.classLoader();
mixinAnnotations = new JsonMixinAnnotations(config);
exactCodecs = new IdentityHashMap<>();
@@ -957,7 +959,11 @@ public final class JsonSharedRegistry {
return new ResolvedCodec(ScalarCodecs.OptionalIntCodec.NON_NULL, null);
}
if (rawType == OptionalLong.class) {
- return new ResolvedCodec(ScalarCodecs.OptionalLongCodec.NON_NULL,
null);
+ return new ResolvedCodec(
+ writeLongAsString
+ ? ScalarCodecs.OptionalLongAsStringCodec.NON_NULL
+ : ScalarCodecs.OptionalLongCodec.NON_NULL,
+ null);
}
return new ResolvedCodec(ScalarCodecs.OptionalDoubleCodec.NON_NULL,
null);
}
@@ -1215,6 +1221,10 @@ public final class JsonSharedRegistry {
return writeNullFields;
}
+ boolean writeLongAsString() {
+ return writeLongAsString;
+ }
+
ClassLoader classLoader() {
return classLoader;
}
@@ -2146,8 +2156,14 @@ public final class JsonSharedRegistry {
exactCodecs.put(Boolean.class, ScalarCodecs.BooleanCodec.BOXED);
exactCodecs.put(int.class, ScalarCodecs.IntCodec.PRIMITIVE);
exactCodecs.put(Integer.class, ScalarCodecs.IntCodec.BOXED);
- exactCodecs.put(long.class, ScalarCodecs.LongCodec.PRIMITIVE);
- exactCodecs.put(Long.class, ScalarCodecs.LongCodec.BOXED);
+ exactCodecs.put(
+ long.class,
+ writeLongAsString
+ ? ScalarCodecs.LongAsStringCodec.PRIMITIVE
+ : ScalarCodecs.LongCodec.PRIMITIVE);
+ exactCodecs.put(
+ Long.class,
+ writeLongAsString ? ScalarCodecs.LongAsStringCodec.BOXED :
ScalarCodecs.LongCodec.BOXED);
exactCodecs.put(short.class, ScalarCodecs.ShortCodec.PRIMITIVE);
exactCodecs.put(Short.class, ScalarCodecs.ShortCodec.BOXED);
exactCodecs.put(byte.class, ScalarCodecs.ByteCodec.PRIMITIVE);
@@ -2168,8 +2184,16 @@ public final class JsonSharedRegistry {
exactCodecs.put(AtomicBoolean.class,
ScalarCodecs.AtomicBooleanCodec.INSTANCE);
exactCodecs.put(AtomicInteger.class,
ScalarCodecs.AtomicIntegerCodec.INSTANCE);
exactCodecs.put(AtomicIntegerArray.class,
ScalarCodecs.AtomicIntegerArrayCodec.INSTANCE);
- exactCodecs.put(AtomicLong.class, ScalarCodecs.AtomicLongCodec.INSTANCE);
- exactCodecs.put(AtomicLongArray.class,
ScalarCodecs.AtomicLongArrayCodec.INSTANCE);
+ exactCodecs.put(
+ AtomicLong.class,
+ writeLongAsString
+ ? ScalarCodecs.AtomicLongAsStringCodec.INSTANCE
+ : ScalarCodecs.AtomicLongCodec.INSTANCE);
+ exactCodecs.put(
+ AtomicLongArray.class,
+ writeLongAsString
+ ? ScalarCodecs.AtomicLongArrayAsStringCodec.INSTANCE
+ : ScalarCodecs.AtomicLongArrayCodec.INSTANCE);
exactCodecs.put(Currency.class, ScalarCodecs.CurrencyCodec.INSTANCE);
exactCodecs.put(File.class, ScalarCodecs.FileCodec.INSTANCE);
exactCodecs.put(URI.class, ScalarCodecs.UriCodec.INSTANCE);
@@ -2201,7 +2225,11 @@ public final class JsonSharedRegistry {
exactCodecs.put(MinguoDate.class, ScalarCodecs.MinguoDateCodec.INSTANCE);
exactCodecs.put(ThaiBuddhistDate.class,
ScalarCodecs.ThaiBuddhistDateCodec.INSTANCE);
exactCodecs.put(OptionalInt.class, ScalarCodecs.OptionalIntCodec.INSTANCE);
- exactCodecs.put(OptionalLong.class,
ScalarCodecs.OptionalLongCodec.INSTANCE);
+ exactCodecs.put(
+ OptionalLong.class,
+ writeLongAsString
+ ? ScalarCodecs.OptionalLongAsStringCodec.INSTANCE
+ : ScalarCodecs.OptionalLongCodec.INSTANCE);
exactCodecs.put(OptionalDouble.class,
ScalarCodecs.OptionalDoubleCodec.INSTANCE);
exactCodecs.put(ByteBuffer.class, ScalarCodecs.ByteBufferCodec.INSTANCE);
GuavaCodecs.registerExactCodecs(exactCodecs);
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 14357eb25..5ee441691 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
@@ -138,6 +138,12 @@ public final class JsonTypeResolver {
return sharedRegistry;
}
+ /** Returns whether this runtime writes default 64-bit integral values as
JSON strings. */
+ @Internal
+ public boolean writeLongAsString() {
+ return sharedRegistry.writeLongAsString();
+ }
+
@Internal
public void lockJIT() {
jitContext.lock();
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 15c88f9ea..4b72e2b37 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
@@ -109,6 +109,9 @@ public abstract class JsonWriter {
public abstract void writeLong(long value);
+ /** Writes a signed 64-bit value as a quoted decimal JSON string. */
+ public abstract void writeLongAsString(long value);
+
/** Writes raw unsigned 32-bit bits as a decimal JSON number. */
public void writeUnsignedInt(int value) {
writeLong(Integer.toUnsignedLong(value));
@@ -117,6 +120,9 @@ public abstract class JsonWriter {
/** Writes raw unsigned 64-bit bits as a decimal JSON number. */
public abstract void writeUnsignedLong(long value);
+ /** Writes raw unsigned 64-bit bits as a quoted decimal JSON string. */
+ public abstract void writeUnsignedLongAsString(long value);
+
public abstract void writeFloat(float value);
public abstract void writeDouble(double value);
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 9c10f102e..eb17bc32e 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
@@ -203,6 +203,25 @@ public final class StringJsonWriter extends JsonWriter
implements Appendable {
writeLongUtf16(value);
}
+ @Override
+ public void writeLongAsString(long value) {
+ if (coder == LATIN1) {
+ if (position + 22 > buffer.length) {
+ grow(22);
+ }
+ buffer[position++] = (byte) '"';
+ writeLongLatin1NoEnsure(value);
+ buffer[position++] = (byte) '"';
+ return;
+ }
+ if (position + 44 > buffer.length) {
+ grow(44);
+ }
+ writeUtf16ByteNoEnsure((byte) '"');
+ writeLongUtf16NoEnsure(value);
+ writeUtf16ByteNoEnsure((byte) '"');
+ }
+
@Override
public void writeUnsignedLong(long value) {
if (value >= 0) {
@@ -215,6 +234,25 @@ public final class StringJsonWriter extends JsonWriter
implements Appendable {
writeByteRaw((byte) ('0' + remainder));
}
+ @Override
+ public void writeUnsignedLongAsString(long value) {
+ if (coder == LATIN1) {
+ if (position + 22 > buffer.length) {
+ grow(22);
+ }
+ buffer[position++] = (byte) '"';
+ writeUnsignedLongLatin1NoEnsure(value);
+ buffer[position++] = (byte) '"';
+ return;
+ }
+ if (position + 44 > buffer.length) {
+ grow(44);
+ }
+ writeUtf16ByteNoEnsure((byte) '"');
+ writeUnsignedLongUtf16NoEnsure(value);
+ writeUtf16ByteNoEnsure((byte) '"');
+ }
+
private void writeLongLatin1(long value) {
if (value == Long.MIN_VALUE) {
writeRaw(MIN_LONG_BYTES);
@@ -1122,6 +1160,199 @@ public final class StringJsonWriter extends JsonWriter
implements Appendable {
writeLongUtf16NoEnsure(value);
}
+ public void writeLongAsStringField(
+ byte[] namePrefix, byte[] commaNamePrefix, int index, long value) {
+ writeLongAsStringField(index == 0 ? namePrefix : commaNamePrefix, value);
+ }
+
+ public void writeLongAsStringField(
+ byte[] namePrefix,
+ byte[] commaNamePrefix,
+ byte[] utf16NamePrefix,
+ byte[] utf16CommaNamePrefix,
+ int index,
+ long value) {
+ if (coder == LATIN1) {
+ writeLongAsStringField(index == 0 ? namePrefix : commaNamePrefix, value);
+ return;
+ }
+ writeLongAsStringFieldUtf16Value(index == 0 ? utf16NamePrefix :
utf16CommaNamePrefix, value);
+ }
+
+ public void writeLongAsStringField(byte[] prefix, long value) {
+ if (coder == LATIN1) {
+ writeLongAsStringFieldLatin1(prefix, value);
+ return;
+ }
+ writeLongAsStringFieldUtf16(prefix, value);
+ }
+
+ public void writeLongAsStringField(byte[] prefix, byte[] utf16Prefix, long
value) {
+ if (coder == LATIN1) {
+ writeLongAsStringFieldLatin1(prefix, value);
+ return;
+ }
+ writeLongAsStringFieldUtf16Value(utf16Prefix, value);
+ }
+
+ public void writeLongAsStringField(
+ byte[] prefix,
+ long utf16Prefix0,
+ long utf16Prefix1,
+ long utf16Prefix2,
+ long utf16Prefix3,
+ int utf16PrefixLength,
+ long value) {
+ if (coder == LATIN1) {
+ writeLongAsStringFieldLatin1(prefix, value);
+ return;
+ }
+ writeLongAsStringFieldUtf16Packed(
+ utf16Prefix0, utf16Prefix1, utf16Prefix2, utf16Prefix3,
utf16PrefixLength, value);
+ }
+
+ private void writeLongAsStringFieldLatin1(byte[] prefix, long value) {
+ int additional = prefix.length + 22;
+ if (position + additional > buffer.length) {
+ grow(additional);
+ }
+ writeRawLatin1NoEnsure(prefix);
+ buffer[position++] = (byte) '"';
+ writeLongLatin1NoEnsure(value);
+ buffer[position++] = (byte) '"';
+ }
+
+ private void writeLongAsStringFieldUtf16(byte[] prefix, long value) {
+ int additional = (prefix.length << 1) + 44;
+ if (position + additional > buffer.length) {
+ grow(additional);
+ }
+ writeRawUtf16NoEnsure(prefix);
+ writeUtf16ByteNoEnsure((byte) '"');
+ writeLongUtf16NoEnsure(value);
+ writeUtf16ByteNoEnsure((byte) '"');
+ }
+
+ private void writeLongAsStringFieldUtf16Value(byte[] utf16Prefix, long
value) {
+ int additional = utf16Prefix.length + 44;
+ if (position + additional > buffer.length) {
+ grow(additional);
+ }
+ writeRawUtf16ValueNoEnsure(utf16Prefix);
+ writeUtf16ByteNoEnsure((byte) '"');
+ writeLongUtf16NoEnsure(value);
+ writeUtf16ByteNoEnsure((byte) '"');
+ }
+
+ private void writeLongAsStringFieldUtf16Packed(
+ long utf16Prefix0,
+ long utf16Prefix1,
+ long utf16Prefix2,
+ long utf16Prefix3,
+ int utf16PrefixLength,
+ long value) {
+ int additional = Math.max(packedUtf16PrefixSize(utf16PrefixLength),
utf16PrefixLength + 44);
+ if (position + additional > buffer.length) {
+ grow(additional);
+ }
+ writePackedUtf16ValueNoEnsure(
+ utf16Prefix0, utf16Prefix1, utf16Prefix2, utf16Prefix3,
utf16PrefixLength);
+ writeUtf16ByteNoEnsure((byte) '"');
+ writeLongUtf16NoEnsure(value);
+ writeUtf16ByteNoEnsure((byte) '"');
+ }
+
+ public void writeObjectStartWithLongAsStringField(byte[] namePrefix, long
value) {
+ enterDepth();
+ if (coder == LATIN1) {
+ writeObjectStartWithLongAsStringFieldLatin1(namePrefix, value);
+ return;
+ }
+ writeObjectStartWithLongAsStringFieldUtf16(namePrefix, value);
+ }
+
+ public void writeObjectStartWithLongAsStringField(
+ byte[] namePrefix, byte[] utf16NamePrefix, long value) {
+ enterDepth();
+ if (coder == LATIN1) {
+ writeObjectStartWithLongAsStringFieldLatin1(namePrefix, value);
+ return;
+ }
+ writeObjectStartWithLongAsStringFieldUtf16Value(utf16NamePrefix, value);
+ }
+
+ public void writeObjectStartWithLongAsStringField(
+ byte[] namePrefix,
+ long utf16Prefix0,
+ long utf16Prefix1,
+ long utf16Prefix2,
+ long utf16Prefix3,
+ int utf16PrefixLength,
+ long value) {
+ enterDepth();
+ if (coder == LATIN1) {
+ writeObjectStartWithLongAsStringFieldLatin1(namePrefix, value);
+ return;
+ }
+ writeObjectStartWithLongAsStringFieldUtf16Packed(
+ utf16Prefix0, utf16Prefix1, utf16Prefix2, utf16Prefix3,
utf16PrefixLength, value);
+ }
+
+ private void writeObjectStartWithLongAsStringFieldLatin1(byte[] namePrefix,
long value) {
+ int additional = namePrefix.length + 23;
+ if (position + additional > buffer.length) {
+ grow(additional);
+ }
+ buffer[position++] = (byte) '{';
+ writeRawLatin1NoEnsure(namePrefix);
+ buffer[position++] = (byte) '"';
+ writeLongLatin1NoEnsure(value);
+ buffer[position++] = (byte) '"';
+ }
+
+ private void writeObjectStartWithLongAsStringFieldUtf16(byte[] namePrefix,
long value) {
+ int additional = ((namePrefix.length + 1) << 1) + 44;
+ if (position + additional > buffer.length) {
+ grow(additional);
+ }
+ writeUtf16ByteNoEnsure((byte) '{');
+ writeRawUtf16NoEnsure(namePrefix);
+ writeUtf16ByteNoEnsure((byte) '"');
+ writeLongUtf16NoEnsure(value);
+ writeUtf16ByteNoEnsure((byte) '"');
+ }
+
+ private void writeObjectStartWithLongAsStringFieldUtf16Value(byte[]
utf16NamePrefix, long value) {
+ int additional = utf16NamePrefix.length + 46;
+ if (position + additional > buffer.length) {
+ grow(additional);
+ }
+ writeUtf16ByteNoEnsure((byte) '{');
+ writeRawUtf16ValueNoEnsure(utf16NamePrefix);
+ writeUtf16ByteNoEnsure((byte) '"');
+ writeLongUtf16NoEnsure(value);
+ writeUtf16ByteNoEnsure((byte) '"');
+ }
+
+ private void writeObjectStartWithLongAsStringFieldUtf16Packed(
+ long utf16Prefix0,
+ long utf16Prefix1,
+ long utf16Prefix2,
+ long utf16Prefix3,
+ int utf16PrefixLength,
+ long value) {
+ int additional = Math.max(packedUtf16PrefixSize(utf16PrefixLength),
utf16PrefixLength + 46);
+ if (position + additional > buffer.length) {
+ grow(additional);
+ }
+ writeUtf16ByteNoEnsure((byte) '{');
+ writePackedUtf16ValueNoEnsure(
+ utf16Prefix0, utf16Prefix1, utf16Prefix2, utf16Prefix3,
utf16PrefixLength);
+ writeUtf16ByteNoEnsure((byte) '"');
+ writeLongUtf16NoEnsure(value);
+ writeUtf16ByteNoEnsure((byte) '"');
+ }
+
public void writeStringField(byte[] namePrefix, byte[] commaNamePrefix, int
index, String value) {
writeRaw(index == 0 ? namePrefix : commaNamePrefix);
writeString(value);
@@ -2507,6 +2738,17 @@ public final class StringJsonWriter extends JsonWriter
implements Appendable {
writePositiveLongLatin1NoEnsure(value);
}
+ private void writeUnsignedLongLatin1NoEnsure(long value) {
+ if (value >= 0) {
+ writeLongLatin1NoEnsure(value);
+ return;
+ }
+ long quotient = Long.divideUnsigned(value, 10);
+ int remainder = (int) Long.remainderUnsigned(value, 10);
+ writeLongLatin1NoEnsure(quotient);
+ buffer[position++] = (byte) ('0' + remainder);
+ }
+
private void writePositiveLongLatin1NoEnsure(long value) {
if (value <= Integer.MAX_VALUE) {
writePositiveIntNoEnsure((int) value);
@@ -2563,6 +2805,17 @@ public final class StringJsonWriter extends JsonWriter
implements Appendable {
position = writePositiveLongUtf16(bytes, pos, value);
}
+ private void writeUnsignedLongUtf16NoEnsure(long value) {
+ if (value >= 0) {
+ writeLongUtf16NoEnsure(value);
+ return;
+ }
+ long quotient = Long.divideUnsigned(value, 10);
+ int remainder = (int) Long.remainderUnsigned(value, 10);
+ writeLongUtf16NoEnsure(quotient);
+ writeUtf16ByteNoEnsure((byte) ('0' + remainder));
+ }
+
private void writePositiveIntNoEnsure(int value) {
byte[] bytes = buffer;
int pos = position;
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 7a612e5a2..652ef3439 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
@@ -188,6 +188,16 @@ public final class Utf8JsonWriter extends JsonWriter
implements Appendable {
writeLongNoEnsure(value);
}
+ @Override
+ public void writeLongAsString(long value) {
+ if (position + 22 > buffer.length) {
+ grow(22);
+ }
+ buffer[position++] = (byte) '"';
+ writeLongNoEnsure(value);
+ buffer[position++] = (byte) '"';
+ }
+
@Override
public void writeUnsignedLong(long value) {
if (value >= 0) {
@@ -200,6 +210,23 @@ public final class Utf8JsonWriter extends JsonWriter
implements Appendable {
buffer[position++] = (byte) ('0' + remainder);
}
+ @Override
+ public void writeUnsignedLongAsString(long value) {
+ if (position + 22 > buffer.length) {
+ grow(22);
+ }
+ buffer[position++] = (byte) '"';
+ if (value >= 0) {
+ writeLongNoEnsure(value);
+ } else {
+ long quotient = Long.divideUnsigned(value, 10);
+ int remainder = (int) Long.remainderUnsigned(value, 10);
+ writeLongNoEnsure(quotient);
+ buffer[position++] = (byte) ('0' + remainder);
+ }
+ buffer[position++] = (byte) '"';
+ }
+
@Override
public void writeFloat(float value) {
if (!Float.isFinite(value)) {
@@ -978,6 +1005,76 @@ public final class Utf8JsonWriter extends JsonWriter
implements Appendable {
writeLongFieldNoEnsure(value);
}
+ public void writeLongAsStringField(
+ byte[] namePrefix, byte[] commaNamePrefix, int index, long value) {
+ writeLongAsStringField(index == 0 ? namePrefix : commaNamePrefix, value);
+ }
+
+ public void writeLongAsStringField(byte[] prefix, long value) {
+ int additional = prefix.length + 22;
+ if (position + additional > buffer.length) {
+ grow(additional);
+ }
+ writeRawNoEnsure(prefix);
+ buffer[position++] = (byte) '"';
+ writeLongFieldNoEnsure(value);
+ buffer[position++] = (byte) '"';
+ }
+
+ public void writeLongAsStringField(long prefix0, long prefix1, int
prefixLength, long value) {
+ int additional = Math.max(packedPrefixSize(prefixLength), prefixLength +
22);
+ if (position + additional > buffer.length) {
+ grow(additional);
+ }
+ writePackedRawNoEnsure(prefix0, prefix1, prefixLength);
+ buffer[position++] = (byte) '"';
+ writeLongFieldNoEnsure(value);
+ buffer[position++] = (byte) '"';
+ }
+
+ public void writeLongAsStringField(
+ long namePrefix0,
+ long namePrefix1,
+ long commaPrefix0,
+ long commaPrefix1,
+ int namePrefixLength,
+ int commaPrefixLength,
+ int index,
+ long value) {
+ if (index == 0) {
+ writeLongAsStringField(namePrefix0, namePrefix1, namePrefixLength,
value);
+ } else {
+ writeLongAsStringField(commaPrefix0, commaPrefix1, commaPrefixLength,
value);
+ }
+ }
+
+ public void writeObjectStartWithLongAsStringField(byte[] namePrefix, long
value) {
+ enterDepth();
+ int additional = namePrefix.length + 23;
+ if (position + additional > buffer.length) {
+ grow(additional);
+ }
+ buffer[position++] = (byte) '{';
+ writeRawNoEnsure(namePrefix);
+ buffer[position++] = (byte) '"';
+ writeLongFieldNoEnsure(value);
+ buffer[position++] = (byte) '"';
+ }
+
+ public void writeObjectStartWithLongAsStringField(
+ long prefix0, long prefix1, int prefixLength, long value) {
+ enterDepth();
+ int additional = Math.max(packedPrefixSize(prefixLength), prefixLength +
23);
+ if (position + additional > buffer.length) {
+ grow(additional);
+ }
+ buffer[position++] = (byte) '{';
+ writePackedRawNoEnsure(prefix0, prefix1, prefixLength);
+ buffer[position++] = (byte) '"';
+ writeLongFieldNoEnsure(value);
+ buffer[position++] = (byte) '"';
+ }
+
public void writeObjectStartWithRawValue(long prefix0, long prefix1, int
prefixLength) {
// Generated codecs prepack '{' with the first field prefix so this
writer-owned buffer update
// needs one capacity check. Keep the String value in the generated
caller: writeString is a
@@ -1175,6 +1272,119 @@ public final class Utf8JsonWriter extends JsonWriter
implements Appendable {
exitDepth();
}
+ /**
+ * Writes a long array whose elements are quoted decimal strings.
+ *
+ * <p>This intentionally preserves the independent, pair-unrolled
generated-caller boundary of
+ * {@link #writeLongArray(long[])}. Keep the signed-Long formatting in each
lane so generated
+ * callers do not absorb the complete array loop after a small element
helper is inlined.
+ */
+ public void writeLongArrayAsString(long[] values) {
+ enterDepth();
+ if (position + 2 > buffer.length) {
+ grow(2);
+ }
+ buffer[position++] = '[';
+ int length = values.length;
+ if (length != 0) {
+ if (position + 24 > buffer.length) {
+ grow(24);
+ }
+ buffer[position++] = '"';
+ {
+ long value = values[0];
+ if (value == Long.MIN_VALUE) {
+ writeRawNoEnsure(MIN_LONG_BYTES);
+ } else {
+ if (value < 0) {
+ buffer[position++] = (byte) '-';
+ value = -value;
+ }
+ if (value <= Integer.MAX_VALUE) {
+ writePositiveIntNoEnsure((int) value);
+ } else {
+ position = writePositiveLong(buffer, position, value);
+ }
+ }
+ }
+ buffer[position++] = '"';
+
+ int i = 1;
+ if ((length & 1) == 0) {
+ if (position + 24 > buffer.length) {
+ grow(24);
+ }
+ buffer[position++] = ',';
+ buffer[position++] = '"';
+ {
+ long value = values[i];
+ if (value == Long.MIN_VALUE) {
+ writeRawNoEnsure(MIN_LONG_BYTES);
+ } else {
+ if (value < 0) {
+ buffer[position++] = (byte) '-';
+ value = -value;
+ }
+ if (value <= Integer.MAX_VALUE) {
+ writePositiveIntNoEnsure((int) value);
+ } else {
+ position = writePositiveLong(buffer, position, value);
+ }
+ }
+ }
+ buffer[position++] = '"';
+ i++;
+ }
+
+ for (; i < length; i += 2) {
+ if (position + 48 > buffer.length) {
+ grow(48);
+ }
+ buffer[position++] = ',';
+ buffer[position++] = '"';
+ {
+ long value = values[i];
+ if (value == Long.MIN_VALUE) {
+ writeRawNoEnsure(MIN_LONG_BYTES);
+ } else {
+ if (value < 0) {
+ buffer[position++] = (byte) '-';
+ value = -value;
+ }
+ if (value <= Integer.MAX_VALUE) {
+ writePositiveIntNoEnsure((int) value);
+ } else {
+ position = writePositiveLong(buffer, position, value);
+ }
+ }
+ }
+ buffer[position++] = '"';
+
+ buffer[position++] = ',';
+ buffer[position++] = '"';
+ {
+ long value = values[i + 1];
+ if (value == Long.MIN_VALUE) {
+ writeRawNoEnsure(MIN_LONG_BYTES);
+ } else {
+ if (value < 0) {
+ buffer[position++] = (byte) '-';
+ value = -value;
+ }
+ if (value <= Integer.MAX_VALUE) {
+ writePositiveIntNoEnsure((int) value);
+ } else {
+ position = writePositiveLong(buffer, position, value);
+ }
+ }
+ }
+ buffer[position++] = '"';
+ }
+ }
+ buffer[position++] = ']';
+ exitDepth();
+ }
+
public void writeStringElement(int index, String value) {
writeStringElementWithComma(index == 0 ? 0 : 1, value);
}
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 65e923db9..7be44bb4d 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
@@ -1316,6 +1316,7 @@ public class JsonAsyncCompilationTest {
throws Exception {
JsonConfig config =
new JsonConfig(
+ false,
false,
true,
true,
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 619b2dc5a..07ee115fd 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
@@ -128,9 +128,10 @@ public class JsonContainerTest extends ForyJsonTestModels {
@Test
public void unsignedArrayOverflow() {
byte[] input = "[4294967295]".getBytes(StandardCharsets.UTF_8);
- ArrayCodec<byte[]> uint8 =
ArrayCodec.createUnsignedPrimitive(byte[].class, Types.UINT8_ARRAY);
+ ArrayCodec<byte[]> uint8 =
+ ArrayCodec.createUnsignedPrimitive(byte[].class, Types.UINT8_ARRAY,
false);
ArrayCodec<short[]> uint16 =
- ArrayCodec.createUnsignedPrimitive(short[].class, Types.UINT16_ARRAY);
+ ArrayCodec.createUnsignedPrimitive(short[].class, Types.UINT16_ARRAY,
false);
assertThrows(ForyJsonException.class, () ->
uint8.readUtf8(newUtf8Reader(input)));
assertThrows(ForyJsonException.class, () ->
uint8.readLatin1(newLatin1Reader(input)));
diff --git
a/java/fory-json/src/test/java/org/apache/fory/json/JsonLongAsStringTest.java
b/java/fory-json/src/test/java/org/apache/fory/json/JsonLongAsStringTest.java
new file mode 100644
index 000000000..466f06673
--- /dev/null
+++
b/java/fory-json/src/test/java/org/apache/fory/json/JsonLongAsStringTest.java
@@ -0,0 +1,309 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.fory.json;
+
+import static org.apache.fory.json.JsonTestSupport.generatedUtf8WriterClass;
+import static org.apache.fory.json.JsonTestSupport.newStringWriter;
+import static org.apache.fory.json.JsonTestSupport.newUtf8Reader;
+import static org.apache.fory.json.JsonTestSupport.newUtf8Writer;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNotSame;
+import static org.testng.Assert.assertThrows;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.BitSet;
+import java.util.Date;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.OptionalLong;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.concurrent.atomic.AtomicLongArray;
+import java.util.concurrent.atomic.AtomicReference;
+import org.apache.fory.json.annotation.JsonCodec;
+import org.apache.fory.json.annotation.JsonProperty;
+import org.apache.fory.json.codec.AbstractJsonValueCodec;
+import org.apache.fory.json.reader.JsonReader;
+import org.apache.fory.json.writer.JsonWriter;
+import org.apache.fory.json.writer.StringJsonWriter;
+import org.apache.fory.json.writer.Utf8JsonWriter;
+import org.apache.fory.reflect.TypeRef;
+import org.testng.annotations.Test;
+
+public class JsonLongAsStringTest extends ForyJsonTestModels {
+ private static final long UNSAFE_INTEGER = 9_007_199_254_740_992L;
+
+ @Test(dataProvider = "enableCodegen")
+ public void writesLongBindings(boolean codegen) {
+ ForyJson json =
newJsonBuilder(codegen).writeLongAsString(true).withConcurrencyLevel(1).build();
+ LongValues value = LongValues.create();
+ String expectedPrefix =
+
"{\"aFirst\":\"-9223372036854775808\",\"boxed\":\"9223372036854775807\","
+ +
"\"boxedArray\":[\"-1\",null,\"0\"],\"dynamic\":\"9007199254740992\","
+ + "\"list\":[\"1\",\"9007199254740992\"],\"map\":{\"max\":"
+ +
"\"9223372036854775807\"},\"primitiveArray\":[\"-9223372036854775808\","
+ + "\"0\",\"9223372036854775807\"],\"text\":\"雪\",";
+ String expected = expectedPrefix + "\"\\u603b\\u6570\":\"7\"}";
+
+ String stringJson = json.toJson(value);
+ String utf8Json = new String(json.toJsonBytes(value),
StandardCharsets.UTF_8);
+ assertEquals(normalizeUnicodeName(stringJson), expected);
+ assertEquals(normalizeUnicodeName(utf8Json), expected);
+ assertEquals(json.toJson(Long.MIN_VALUE), "\"-9223372036854775808\"");
+ assertEquals(
+ new String(json.toJsonBytes(Long.MAX_VALUE), StandardCharsets.UTF_8),
+ "\"9223372036854775807\"");
+ assertEquals(json.toJson(new long[] {-1, 0, 1}), "[\"-1\",\"0\",\"1\"]");
+ assertEquals(json.toJson(new Long[] {-1L, null, 1L}),
"[\"-1\",null,\"1\"]");
+ assertEquals(json.toJson(Arrays.asList(-1L, 1L)), "[\"-1\",\"1\"]");
+
+ LongValues decoded = json.fromJson(stringJson, LongValues.class);
+ assertEquals(decoded.aFirst, Long.MIN_VALUE);
+ assertEquals(decoded.boxed, Long.valueOf(Long.MAX_VALUE));
+ assertEquals(decoded.primitiveArray, value.primitiveArray);
+ assertEquals(decoded.boxedArray, value.boxedArray);
+ assertEquals(decoded.list, value.list);
+ assertEquals(decoded.map, value.map);
+ assertEquals(decoded.unicode, 7L);
+ }
+
+ @Test(dataProvider = "enableCodegen")
+ public void keepsNumericDefault(boolean codegen) {
+ ForyJson json = newJson(codegen);
+ assertEquals(json.toJson(Long.MIN_VALUE), "-9223372036854775808");
+ assertEquals(json.toJson(new long[] {-1, 0, 1}), "[-1,0,1]");
+ assertEquals(json.toJson(Arrays.asList(-1L, 1L)), "[-1,1]");
+ assertEquals(json.toJson(new AtomicLong(Long.MAX_VALUE)),
"9223372036854775807");
+ assertEquals(json.toJson(new AtomicLongArray(new long[] {-1, 0})),
"[-1,0]");
+ assertEquals(json.toJson(OptionalLong.of(Long.MIN_VALUE)),
"-9223372036854775808");
+ }
+
+ @Test(dataProvider = "enableCodegen")
+ public void readsBothTokenShapes(boolean codegen) {
+ ForyJson json = newJson(codegen);
+ assertEquals(json.fromJson("9223372036854775807", long.class),
Long.MAX_VALUE);
+ assertEquals(json.fromJson("\"9223372036854775807\"", long.class),
Long.MAX_VALUE);
+ assertEquals(
+ json.fromJson("[\"-9223372036854775808\",0]", long[].class),
+ new long[] {Long.MIN_VALUE, 0});
+ assertEquals(
+ json.fromJson(
+
"{\"aFirst\":\"-1\",\"boxed\":2,\"boxedArray\":[],\"dynamic\":3,"
+ +
"\"list\":[],\"map\":{},\"primitiveArray\":[],\"总数\":\"4\"}",
+ LongValues.class)
+ .unicode,
+ 4L);
+ }
+
+ @Test
+ public void writesUnsignedDigitsAsString() {
+ StringJsonWriter stringWriter = newStringWriter();
+ stringWriter.writeUnsignedLongAsString(-1L);
+ assertEquals(stringWriter.toJson(), "\"18446744073709551615\"");
+
+ Utf8JsonWriter utf8Writer = newUtf8Writer();
+ utf8Writer.writeUnsignedLongAsString(-1L);
+ assertEquals(
+ new String(utf8Writer.toJsonBytes(), StandardCharsets.UTF_8),
"\"18446744073709551615\"");
+
+ assertEquals(
+
newUtf8Reader("\"18446744073709551615\"".getBytes(StandardCharsets.UTF_8))
+ .readUnsignedLong(),
+ -1L);
+ assertThrows(
+ ForyJsonException.class,
+ () ->
+
newUtf8Reader("\"18446744073709551616\"".getBytes(StandardCharsets.UTF_8))
+ .readUnsignedLong());
+ }
+
+ @Test
+ public void snapshotsBuilderSetting() {
+ ForyJsonBuilder builder =
ForyJson.builder().withCodegen(false).writeLongAsString(false);
+ ForyJson numeric = builder.build();
+ ForyJson quoted = builder.writeLongAsString(true).build();
+ builder.writeLongAsString(false);
+
+ assertEquals(numeric.toJson(7L), "7");
+ assertEquals(quoted.toJson(7L), "\"7\"");
+ }
+
+ @Test(dataProvider = "enableCodegen")
+ public void writesLongWrappers(boolean codegen) {
+ ForyJson json = newJsonBuilder(codegen).writeLongAsString(true).build();
+ LongWrappers value = LongWrappers.create();
+ String expected =
+ "{\"atomic\":\"9223372036854775807\",\"atomicArray\":[\"-1\",\"0\"],"
+ + "\"optional\":\"9007199254740992\","
+ + "\"optionalLong\":\"-9223372036854775808\",\"reference\":\"7\"}";
+
+ assertEquals(json.toJson(value), expected);
+ assertEquals(new String(json.toJsonBytes(value), StandardCharsets.UTF_8),
expected);
+ assertEquals(json.toJson(new AtomicLong(Long.MAX_VALUE)),
"\"9223372036854775807\"");
+ assertEquals(json.toJson(OptionalLong.of(Long.MIN_VALUE)),
"\"-9223372036854775808\"");
+ assertEquals(
+ json.toJson(Optional.of(Long.MAX_VALUE), new TypeRef<Optional<Long>>()
{}),
+ "\"9223372036854775807\"");
+
+ LongWrappers decoded = json.fromJson(expected, LongWrappers.class);
+ assertEquals(decoded.atomic.get(), Long.MAX_VALUE);
+ assertEquals(decoded.atomicArray.get(0), -1L);
+ assertEquals(decoded.atomicArray.get(1), 0L);
+ assertEquals(decoded.optional, Optional.of(UNSAFE_INTEGER));
+ assertEquals(decoded.optionalLong, OptionalLong.of(Long.MIN_VALUE));
+ assertEquals(decoded.reference.get(), Long.valueOf(7L));
+ }
+
+ @Test
+ public void keepsOtherLongCarriers() {
+ ForyJson numeric = ForyJson.builder().withCodegen(false).build();
+ ForyJson quoted =
ForyJson.builder().withCodegen(false).writeLongAsString(true).build();
+ Object[] values = {
+ new Date(123456789L),
+ BitSet.valueOf(new long[] {Long.MIN_VALUE}),
+ new NumberValue(Long.MAX_VALUE)
+ };
+ for (Object value : values) {
+ assertEquals(quoted.toJson(value), numeric.toJson(value),
value.getClass().getName());
+ }
+ assertEquals(
+ quoted.toJson(new CustomValue(Long.MAX_VALUE)),
"{\"value\":\"long:9223372036854775807\"}");
+ }
+
+ @Test
+ public void isolatesGeneratedWriters() {
+ LongValues value = LongValues.create();
+ ForyJson numeric = ForyJson.builder().withAsyncCompilation(false).build();
+ ForyJson quoted =
+
ForyJson.builder().writeLongAsString(true).withAsyncCompilation(false).build();
+
+ assertEquals(numeric.toJsonBytes(value)[10], (byte) '-');
+ assertEquals(quoted.toJson(value).charAt(10), '"');
+ assertNotSame(
+ generatedUtf8WriterClass(numeric, LongValues.class),
+ generatedUtf8WriterClass(quoted, LongValues.class));
+ }
+
+ public static final class LongValues {
+ @JsonProperty(index = 0)
+ public long aFirst;
+
+ @JsonProperty(index = 1)
+ public Long boxed;
+
+ @JsonProperty(index = 2)
+ public Long[] boxedArray;
+
+ @JsonProperty(index = 3)
+ public Object dynamic;
+
+ @JsonProperty(index = 4)
+ public List<Long> list;
+
+ @JsonProperty(index = 5)
+ public Map<String, Long> map;
+
+ @JsonProperty(index = 6)
+ public long[] primitiveArray;
+
+ @JsonProperty(index = 7)
+ public String text;
+
+ @JsonProperty(value = "总数", index = 8)
+ public long unicode;
+
+ static LongValues create() {
+ LongValues value = new LongValues();
+ value.aFirst = Long.MIN_VALUE;
+ value.boxed = Long.MAX_VALUE;
+ value.boxedArray = new Long[] {-1L, null, 0L};
+ value.dynamic = UNSAFE_INTEGER;
+ value.list = Arrays.asList(1L, UNSAFE_INTEGER);
+ value.map = new LinkedHashMap<>();
+ value.map.put("max", Long.MAX_VALUE);
+ value.primitiveArray = new long[] {Long.MIN_VALUE, 0, Long.MAX_VALUE};
+ value.text = "雪";
+ value.unicode = 7;
+ return value;
+ }
+ }
+
+ private static String normalizeUnicodeName(String json) {
+ return json.replace("\"总数\"", "\"\\u603b\\u6570\"");
+ }
+
+ public static final class NumberValue {
+ public Number value;
+
+ NumberValue(long value) {
+ this.value = value;
+ }
+ }
+
+ public static final class LongWrappers {
+ @JsonProperty(index = 0)
+ public AtomicLong atomic;
+
+ @JsonProperty(index = 1)
+ public AtomicLongArray atomicArray;
+
+ @JsonProperty(index = 2)
+ public Optional<Long> optional;
+
+ @JsonProperty(index = 3)
+ public OptionalLong optionalLong;
+
+ @JsonProperty(index = 4)
+ public AtomicReference<Long> reference;
+
+ static LongWrappers create() {
+ LongWrappers value = new LongWrappers();
+ value.atomic = new AtomicLong(Long.MAX_VALUE);
+ value.atomicArray = new AtomicLongArray(new long[] {-1, 0});
+ value.optional = Optional.of(UNSAFE_INTEGER);
+ value.optionalLong = OptionalLong.of(Long.MIN_VALUE);
+ value.reference = new AtomicReference<>(7L);
+ return value;
+ }
+ }
+
+ public static final class CustomValue {
+ @JsonCodec(LongTextCodec.class)
+ public long value;
+
+ CustomValue(long value) {
+ this.value = value;
+ }
+ }
+
+ public static final class LongTextCodec extends AbstractJsonValueCodec<Long>
{
+ @Override
+ public void write(JsonWriter writer, Long value) {
+ writer.writeString("long:" + value);
+ }
+
+ @Override
+ public Long read(JsonReader reader) {
+ return Long.valueOf(reader.readString().substring(5));
+ }
+ }
+}
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 503ea4851..889d30e71 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
@@ -45,6 +45,7 @@ final class JsonTestSupport {
false,
false,
false,
+ false,
true,
PropertyNamingStrategy.LOWER_CAMEL_CASE,
JsonTestSupport.class.getClassLoader(),
diff --git
a/kotlin/fory-json-kotlin/src/main/kotlin/org/apache/fory/json/kotlin/KotlinJsonCodecFactory.kt
b/kotlin/fory-json-kotlin/src/main/kotlin/org/apache/fory/json/kotlin/KotlinJsonCodecFactory.kt
index 70c2e8a12..1ff68727c 100644
---
a/kotlin/fory-json-kotlin/src/main/kotlin/org/apache/fory/json/kotlin/KotlinJsonCodecFactory.kt
+++
b/kotlin/fory-json-kotlin/src/main/kotlin/org/apache/fory/json/kotlin/KotlinJsonCodecFactory.kt
@@ -43,13 +43,15 @@ internal object KotlinJsonCodecFactory : JsonCodecFactory {
semanticId,
!rawType.isPrimitive,
type.typeExtMeta.nullable(),
+ resolver.writeLongAsString(),
)
}
if (semanticId in Types.UINT8_ARRAY..Types.UINT64_ARRAY) {
- KotlinUnsignedArrayCodecs.create(rawType, semanticId)?.let {
+ val writeLongAsString = resolver.writeLongAsString()
+ KotlinUnsignedArrayCodecs.create(rawType, semanticId,
writeLongAsString)?.let {
return it
}
- return ArrayCodec.createUnsignedPrimitive(rawType, semanticId)
+ return ArrayCodec.createUnsignedPrimitive(rawType, semanticId,
writeLongAsString)
}
if (rawType == Unit::class.java) {
return if (type.typeExtMeta?.nullable() == true) {
diff --git
a/kotlin/fory-json-kotlin/src/main/kotlin/org/apache/fory/json/kotlin/KotlinUnsignedArrayCodecs.kt
b/kotlin/fory-json-kotlin/src/main/kotlin/org/apache/fory/json/kotlin/KotlinUnsignedArrayCodecs.kt
index 724764bf4..a2e6961d0 100644
---
a/kotlin/fory-json-kotlin/src/main/kotlin/org/apache/fory/json/kotlin/KotlinUnsignedArrayCodecs.kt
+++
b/kotlin/fory-json-kotlin/src/main/kotlin/org/apache/fory/json/kotlin/KotlinUnsignedArrayCodecs.kt
@@ -40,12 +40,22 @@ import org.apache.fory.type.Types
*/
@OptIn(ExperimentalUnsignedTypes::class)
internal object KotlinUnsignedArrayCodecs {
- fun create(rawType: Class<*>, typeId: Int): JsonValueCodec<*>? =
+ fun create(
+ rawType: Class<*>,
+ typeId: Int,
+ writeLongAsString: Boolean,
+ ): JsonValueCodec<*>? =
when (rawType) {
UByteArray::class.java -> requireType(rawType, typeId,
Types.UINT8_ARRAY, UByteArrayCodec)
UShortArray::class.java -> requireType(rawType, typeId,
Types.UINT16_ARRAY, UShortArrayCodec)
UIntArray::class.java -> requireType(rawType, typeId,
Types.UINT32_ARRAY, UIntArrayCodec)
- ULongArray::class.java -> requireType(rawType, typeId,
Types.UINT64_ARRAY, ULongArrayCodec)
+ ULongArray::class.java ->
+ requireType(
+ rawType,
+ typeId,
+ Types.UINT64_ARRAY,
+ if (writeLongAsString) ULongArrayCodec.QUOTED else
ULongArrayCodec.NUMERIC,
+ )
else -> null
}
@@ -65,7 +75,7 @@ internal object KotlinUnsignedArrayCodecs {
private object UByteArrayCodec : JsonValueCodec<UByteArray> {
private val delegate =
- ArrayCodec.createUnsignedPrimitive(ByteArray::class.java,
Types.UINT8_ARRAY)
+ ArrayCodec.createUnsignedPrimitive(ByteArray::class.java,
Types.UINT8_ARRAY, false)
private val wrapperBytes =
GraphMemoryEstimates.shallowObjectBytes(UByteArray::class.java)
override fun writeString(writer: StringJsonWriter, value: UByteArray?) =
@@ -95,7 +105,7 @@ internal object KotlinUnsignedArrayCodecs {
private object UShortArrayCodec : JsonValueCodec<UShortArray> {
private val delegate =
- ArrayCodec.createUnsignedPrimitive(ShortArray::class.java,
Types.UINT16_ARRAY)
+ ArrayCodec.createUnsignedPrimitive(ShortArray::class.java,
Types.UINT16_ARRAY, false)
private val wrapperBytes =
GraphMemoryEstimates.shallowObjectBytes(UShortArray::class.java)
override fun writeString(writer: StringJsonWriter, value: UShortArray?) =
@@ -125,7 +135,7 @@ internal object KotlinUnsignedArrayCodecs {
private object UIntArrayCodec : JsonValueCodec<UIntArray> {
private val delegate =
- ArrayCodec.createUnsignedPrimitive(IntArray::class.java,
Types.UINT32_ARRAY)
+ ArrayCodec.createUnsignedPrimitive(IntArray::class.java,
Types.UINT32_ARRAY, false)
private val wrapperBytes =
GraphMemoryEstimates.shallowObjectBytes(UIntArray::class.java)
override fun writeString(writer: StringJsonWriter, value: UIntArray?) =
@@ -150,9 +160,20 @@ internal object KotlinUnsignedArrayCodecs {
}
}
- private object ULongArrayCodec : JsonValueCodec<ULongArray> {
- private val delegate =
- ArrayCodec.createUnsignedPrimitive(LongArray::class.java,
Types.UINT64_ARRAY)
+ private class ULongArrayCodec(
+ private val delegate: JsonValueCodec<LongArray>,
+ ) : JsonValueCodec<ULongArray> {
+ companion object {
+ val NUMERIC =
+ ULongArrayCodec(
+ ArrayCodec.createUnsignedPrimitive(LongArray::class.java,
Types.UINT64_ARRAY, false)
+ )
+ val QUOTED =
+ ULongArrayCodec(
+ ArrayCodec.createUnsignedPrimitive(LongArray::class.java,
Types.UINT64_ARRAY, true)
+ )
+ }
+
private val wrapperBytes =
GraphMemoryEstimates.shallowObjectBytes(ULongArray::class.java)
override fun writeString(writer: StringJsonWriter, value: ULongArray?) =
diff --git
a/kotlin/fory-json-kotlin/src/main/kotlin/org/apache/fory/json/kotlin/KotlinUnsignedCodecs.kt
b/kotlin/fory-json-kotlin/src/main/kotlin/org/apache/fory/json/kotlin/KotlinUnsignedCodecs.kt
index f48524201..054758d67 100644
---
a/kotlin/fory-json-kotlin/src/main/kotlin/org/apache/fory/json/kotlin/KotlinUnsignedCodecs.kt
+++
b/kotlin/fory-json-kotlin/src/main/kotlin/org/apache/fory/json/kotlin/KotlinUnsignedCodecs.kt
@@ -35,7 +35,12 @@ import org.apache.fory.type.Types
/** Width- and carrier-specialized codecs for Kotlin unsigned scalars. */
@OptIn(ExperimentalUnsignedTypes::class)
internal object KotlinUnsignedCodecs {
- fun scalar(typeId: Int, boxedResult: Boolean, nullable: Boolean):
JsonValueCodec<Any?> =
+ fun scalar(
+ typeId: Int,
+ boxedResult: Boolean,
+ nullable: Boolean,
+ writeLongAsString: Boolean,
+ ): JsonValueCodec<Any?> =
when (typeId) {
Types.UINT8 ->
if (nullable) UByteCodec.NULLABLE
@@ -46,8 +51,13 @@ internal object KotlinUnsignedCodecs {
Types.UINT32 ->
if (nullable) UIntCodec.NULLABLE else if (boxedResult) UIntCodec.BOXED
else UIntCarrierCodec
Types.UINT64 ->
- if (nullable) ULongCodec.NULLABLE
- else if (boxedResult) ULongCodec.BOXED else ULongCarrierCodec
+ if (writeLongAsString) {
+ if (nullable) ULongAsStringCodec.NULLABLE
+ else if (boxedResult) ULongAsStringCodec.BOXED else
ULongAsStringCarrierCodec
+ } else {
+ if (nullable) ULongCodec.NULLABLE
+ else if (boxedResult) ULongCodec.BOXED else ULongCarrierCodec
+ }
else -> throw ForyJsonException("Unknown Kotlin unsigned JSON type id
$typeId")
}
@@ -93,6 +103,11 @@ internal object KotlinUnsignedCodecs {
@JvmName("writeULongRaw")
fun writeULongRaw(writer: JsonWriter, value: Long) =
writer.writeUnsignedLong(value)
+ @JvmStatic
+ @JvmName("writeULongAsStringRaw")
+ fun writeULongAsStringRaw(writer: JsonWriter, value: Long) =
+ writer.writeUnsignedLongAsString(value)
+
private class UByteCodec(private val nullable: Boolean) :
JsonValueCodec<Any?> {
companion object {
val BOXED: JsonValueCodec<Any?> = UByteCodec(false)
@@ -377,6 +392,77 @@ internal object KotlinUnsignedCodecs {
override fun writeCarrierMethod(): Method = Methods.write
}
+ private class ULongAsStringCodec(private val nullable: Boolean) :
JsonValueCodec<Any?> {
+ companion object {
+ val BOXED: JsonValueCodec<Any?> = ULongAsStringCodec(false)
+ val NULLABLE: JsonValueCodec<Any?> = ULongAsStringCodec(true)
+ }
+
+ override fun writeString(writer: StringJsonWriter, value: Any?) =
write(writer, value)
+
+ override fun writeUtf8(writer: Utf8JsonWriter, value: Any?) =
write(writer, value)
+
+ override fun readLatin1(reader: Latin1JsonReader): Any? = read(reader)
+
+ override fun readUtf16(reader: Utf16JsonReader): Any? = read(reader)
+
+ override fun readUtf8(reader: Utf8JsonReader): Any? = read(reader)
+
+ private fun write(writer: JsonWriter, value: Any?) {
+ if (value == null) {
+ writeNull(writer, nullable)
+ return
+ }
+ writeULongAsStringRaw(writer, (value as ULong).toLong())
+ }
+
+ private fun read(reader: JsonReader): Any? {
+ if (reader.tryReadNullToken()) return readNull(nullable)
+ return readULongRaw(reader).toULong()
+ }
+ }
+
+ private object ULongAsStringCarrierCodec : JsonValueCodec<Any?>,
DirectUnboxedValueCodec {
+ private object Methods {
+ val read: Method =
+ KotlinUnsignedCodecs::class.java.getMethod("readULongRaw",
JsonReader::class.java)
+ val write: Method =
+ KotlinUnsignedCodecs::class
+ .java
+ .getMethod("writeULongAsStringRaw", JsonWriter::class.java,
java.lang.Long.TYPE)
+ }
+
+ override fun writeString(writer: StringJsonWriter, value: Any?) =
+ writeStringCarrier(writer, value ?: rejectNull())
+
+ override fun writeUtf8(writer: Utf8JsonWriter, value: Any?) =
+ writeUtf8Carrier(writer, value ?: rejectNull())
+
+ override fun readLatin1(reader: Latin1JsonReader): Any =
readLatin1Carrier(reader)
+
+ override fun readUtf16(reader: Utf16JsonReader): Any =
readUtf16Carrier(reader)
+
+ override fun readUtf8(reader: Utf8JsonReader): Any =
readUtf8Carrier(reader)
+
+ override fun carrierType(): Class<*> = java.lang.Long.TYPE
+
+ override fun readLatin1Carrier(reader: Latin1JsonReader): Any =
readULongRaw(reader)
+
+ override fun readUtf16Carrier(reader: Utf16JsonReader): Any =
readULongRaw(reader)
+
+ override fun readUtf8Carrier(reader: Utf8JsonReader): Any =
readULongRaw(reader)
+
+ override fun writeStringCarrier(writer: StringJsonWriter, carrier: Any) =
+ writeULongAsStringRaw(writer, carrier as Long)
+
+ override fun writeUtf8Carrier(writer: Utf8JsonWriter, carrier: Any) =
+ writeULongAsStringRaw(writer, carrier as Long)
+
+ override fun readCarrierMethod(): Method = Methods.read
+
+ override fun writeCarrierMethod(): Method = Methods.write
+ }
+
private fun writeNull(writer: JsonWriter, nullable: Boolean) {
if (!nullable) rejectNull()
writer.writeNull()
diff --git
a/kotlin/fory-json-kotlin/src/main/kotlin/org/apache/fory/json/kotlin/KotlinValueClassPrimitiveCodecs.kt
b/kotlin/fory-json-kotlin/src/main/kotlin/org/apache/fory/json/kotlin/KotlinValueClassPrimitiveCodecs.kt
index 8d756b0b4..f5af42979 100644
---
a/kotlin/fory-json-kotlin/src/main/kotlin/org/apache/fory/json/kotlin/KotlinValueClassPrimitiveCodecs.kt
+++
b/kotlin/fory-json-kotlin/src/main/kotlin/org/apache/fory/json/kotlin/KotlinValueClassPrimitiveCodecs.kt
@@ -51,7 +51,7 @@ internal object KotlinValueClassCapabilities {
}
java.lang.Byte.TYPE -> {
if (typeId == Types.UINT8) {
- val expected = KotlinUnsignedCodecs.scalar(typeId, false, false)
+ val expected = KotlinUnsignedCodecs.scalar(typeId, false, false,
false)
if (exact(child, expected)) {
operations.typed<KotlinByteValueClassOperations<Any>>()?.let {
return UByteCapability(it)
@@ -65,7 +65,7 @@ internal object KotlinValueClassCapabilities {
}
java.lang.Short.TYPE -> {
if (typeId == Types.UINT16) {
- val expected = KotlinUnsignedCodecs.scalar(typeId, false, false)
+ val expected = KotlinUnsignedCodecs.scalar(typeId, false, false,
false)
if (exact(child, expected)) {
operations.typed<KotlinShortValueClassOperations<Any>>()?.let {
return UShortCapability(it)
@@ -79,7 +79,7 @@ internal object KotlinValueClassCapabilities {
}
java.lang.Integer.TYPE -> {
if (typeId == Types.UINT32) {
- val expected = KotlinUnsignedCodecs.scalar(typeId, false, false)
+ val expected = KotlinUnsignedCodecs.scalar(typeId, false, false,
false)
if (exact(child, expected)) {
operations.typed<KotlinIntValueClassOperations<Any>>()?.let {
return UIntCapability(it)
@@ -93,16 +93,28 @@ internal object KotlinValueClassCapabilities {
}
java.lang.Long.TYPE -> {
if (typeId == Types.UINT64) {
- val expected = KotlinUnsignedCodecs.scalar(typeId, false, false)
- if (exact(child, expected)) {
+ val numeric = KotlinUnsignedCodecs.scalar(typeId, false, false,
false)
+ if (exact(child, numeric)) {
operations.typed<KotlinLongValueClassOperations<Any>>()?.let {
return ULongCapability(it)
}
}
+ val quoted = KotlinUnsignedCodecs.scalar(typeId, false, false, true)
+ if (exact(child, quoted)) {
+ operations.typed<KotlinLongValueClassOperations<Any>>()?.let {
+ return ULongAsStringCapability(it)
+ }
+ }
} else if (typeId == Types.UNKNOWN && exact(child,
ScalarCodecs.LongCodec.PRIMITIVE)) {
operations.typed<KotlinLongValueClassOperations<Any>>()?.let {
return LongCapability(it)
}
+ } else if (
+ typeId == Types.UNKNOWN && exact(child,
ScalarCodecs.LongAsStringCodec.PRIMITIVE)
+ ) {
+ operations.typed<KotlinLongValueClassOperations<Any>>()?.let {
+ return LongAsStringCapability(it)
+ }
}
}
java.lang.Float.TYPE ->
@@ -234,6 +246,25 @@ private class LongCapability(
operations.constructLong(reader, reader.readLongValue())
}
+private class LongAsStringCapability(
+ private val operations: KotlinLongValueClassOperations<Any>,
+) : KotlinValueClassCapability {
+ override fun writeString(writer: StringJsonWriter, value: Any) =
+ writer.writeLongAsString(operations.unboxLong(value))
+
+ override fun writeUtf8(writer: Utf8JsonWriter, value: Any) =
+ writer.writeLongAsString(operations.unboxLong(value))
+
+ override fun readLatin1(reader: Latin1JsonReader): Any =
+ operations.constructLong(reader, reader.readLongValue())
+
+ override fun readUtf16(reader: Utf16JsonReader): Any =
+ operations.constructLong(reader, reader.readLongValue())
+
+ override fun readUtf8(reader: Utf8JsonReader): Any =
+ operations.constructLong(reader, reader.readLongValue())
+}
+
private class FloatCapability(
private val operations: KotlinFloatValueClassOperations<Any>,
) : KotlinValueClassCapability {
@@ -366,3 +397,22 @@ private class ULongCapability(
override fun readUtf8(reader: Utf8JsonReader): Any =
operations.constructLong(reader, KotlinUnsignedCodecs.readULongRaw(reader))
}
+
+private class ULongAsStringCapability(
+ private val operations: KotlinLongValueClassOperations<Any>,
+) : KotlinValueClassCapability {
+ override fun writeString(writer: StringJsonWriter, value: Any) =
+ KotlinUnsignedCodecs.writeULongAsStringRaw(writer,
operations.unboxLong(value))
+
+ override fun writeUtf8(writer: Utf8JsonWriter, value: Any) =
+ KotlinUnsignedCodecs.writeULongAsStringRaw(writer,
operations.unboxLong(value))
+
+ override fun readLatin1(reader: Latin1JsonReader): Any =
+ operations.constructLong(reader, KotlinUnsignedCodecs.readULongRaw(reader))
+
+ override fun readUtf16(reader: Utf16JsonReader): Any =
+ operations.constructLong(reader, KotlinUnsignedCodecs.readULongRaw(reader))
+
+ override fun readUtf8(reader: Utf8JsonReader): Any =
+ operations.constructLong(reader, KotlinUnsignedCodecs.readULongRaw(reader))
+}
diff --git
a/kotlin/fory-json-kotlin/src/test/kotlin/org/apache/fory/json/kotlin/ForyJsonKotlinTest.kt
b/kotlin/fory-json-kotlin/src/test/kotlin/org/apache/fory/json/kotlin/ForyJsonKotlinTest.kt
index 662e20d3c..957830806 100644
---
a/kotlin/fory-json-kotlin/src/test/kotlin/org/apache/fory/json/kotlin/ForyJsonKotlinTest.kt
+++
b/kotlin/fory-json-kotlin/src/test/kotlin/org/apache/fory/json/kotlin/ForyJsonKotlinTest.kt
@@ -19,9 +19,11 @@
package org.apache.fory.json.kotlin
+import java.util.concurrent.atomic.AtomicLong
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
+import kotlin.test.assertFalse
import kotlin.test.assertSame
import kotlin.test.assertTrue
import org.apache.fory.json.ForyJsonException
@@ -47,6 +49,24 @@ class ForyJsonKotlinTest {
data class UnsignedValues(val count: UInt, val total: ULong, val optional:
UInt?)
+ @JvmInline value class SignedLongId(val value: Long)
+
+ @JvmInline value class UnsignedLongId(val value: ULong)
+
+ @OptIn(ExperimentalUnsignedTypes::class)
+ data class LongStringValues(
+ val signed: Long,
+ val signedNullable: Long?,
+ val unsigned: ULong,
+ val nullable: ULong?,
+ val unsignedArray: ULongArray,
+ val unsignedList: List<ULong>,
+ val unsignedMap: Map<String, ULong>,
+ val signedId: SignedLongId,
+ val unsignedId: UnsignedLongId,
+ val atomic: AtomicLong,
+ )
+
object Marker
@Test
@@ -136,6 +156,108 @@ class ForyJsonKotlinTest {
assertEquals(listOf(0u, UInt.MAX_VALUE), fory.fromJson("[0,4294967295]",
listType))
}
+ @OptIn(ExperimentalUnsignedTypes::class)
+ @Test
+ fun longAsString() {
+ val value =
+ LongStringValues(
+ Long.MIN_VALUE,
+ 9_007_199_254_740_992L,
+ ULong.MAX_VALUE,
+ 7uL,
+ ulongArrayOf(0uL, ULong.MAX_VALUE),
+ listOf(1uL, ULong.MAX_VALUE),
+ linkedMapOf("max" to ULong.MAX_VALUE),
+ SignedLongId(Long.MAX_VALUE),
+ UnsignedLongId(ULong.MAX_VALUE),
+ AtomicLong(Long.MAX_VALUE),
+ )
+ val type = jsonTypeRef<LongStringValues>()
+ for (mode in listOf(KotlinJsonTestMode.INTERPRETED,
KotlinJsonTestMode.SYNCHRONOUS)) {
+ val json = newKotlinJson(mode) { writeLongAsString(true) }
+ val encoded = json.toJson(value, type)
+ assertTrue(encoded.contains("\"signed\":\"-9223372036854775808\""),
encoded)
+ assertTrue(encoded.contains("\"signedNullable\":\"9007199254740992\""),
encoded)
+ assertTrue(encoded.contains("\"unsigned\":\"18446744073709551615\""),
encoded)
+ assertTrue(encoded.contains("\"nullable\":\"7\""), encoded)
+ assertTrue(
+ encoded.contains("\"unsignedArray\":[\"0\",\"18446744073709551615\"]"),
+ encoded,
+ )
+ assertTrue(
+ encoded.contains("\"unsignedList\":[\"1\",\"18446744073709551615\"]"),
+ encoded,
+ )
+ assertTrue(
+ encoded.contains("\"unsignedMap\":{\"max\":\"18446744073709551615\"}"),
+ encoded,
+ )
+ assertTrue(encoded.contains("\"signedId\":\"9223372036854775807\""),
encoded)
+ assertTrue(
+ encoded.contains("\"unsignedId\":\"18446744073709551615\""),
+ encoded,
+ )
+ assertTrue(encoded.contains("\"atomic\":\"9223372036854775807\""),
encoded)
+ val decoded = json.fromJson(encoded, type)
+ assertEquals(value.signed, decoded.signed)
+ assertEquals(value.signedNullable, decoded.signedNullable)
+ assertEquals(value.unsigned, decoded.unsigned)
+ assertEquals(value.nullable, decoded.nullable)
+ assertTrue(value.unsignedArray.contentEquals(decoded.unsignedArray))
+ assertEquals(value.unsignedList, decoded.unsignedList)
+ assertEquals(value.unsignedMap, decoded.unsignedMap)
+ assertEquals(value.signedId, decoded.signedId)
+ assertEquals(value.unsignedId, decoded.unsignedId)
+ assertEquals(value.atomic.get(), decoded.atomic.get())
+ assertEquals(encoded, json.toJsonBytes(value,
type).toString(Charsets.UTF_8))
+ assertEquals(ULong.MAX_VALUE, json.fromJson("18446744073709551615",
jsonTypeRef<ULong>()))
+ assertEquals(
+ ULong.MAX_VALUE,
+ json.fromJson("\"18446744073709551615\"", jsonTypeRef<ULong>()),
+ )
+ assertFailsWith<ForyJsonException> {
+ json.fromJson("\"18446744073709551616\"", jsonTypeRef<ULong>())
+ }
+
+ if (mode == KotlinJsonTestMode.SYNCHRONOUS) {
+ val refs =
+ generatedClassBytes(json, "LongStringVal")
+ .filterKeys { it.contains("WriterForyJsonCodec") }
+ .values
+ .flatMap(::generatedMethodRefs)
+ assertTrue(
+ refs.any {
+ it.owner == "org/apache/fory/json/kotlin/KotlinUnsignedCodecs" &&
+ it.name == "writeULongAsStringRaw" &&
+ it.descriptor == "(Lorg/apache/fory/json/writer/JsonWriter;J)V"
+ },
+ refs.toString(),
+ )
+ assertTrue(
+ refs.any {
+ it.owner == "org/apache/fory/json/writer/StringJsonWriter" &&
+ it.name == "writeLongAsStringField"
+ },
+ refs.toString(),
+ )
+ assertTrue(
+ refs.any {
+ it.owner == "org/apache/fory/json/writer/Utf8JsonWriter" &&
+ it.name == "writeLongAsStringField"
+ },
+ refs.toString(),
+ )
+ assertFalse(
+ refs.any {
+ it.owner == "org/apache/fory/json/resolver/JsonTypeResolver" &&
+ it.name == "writeLongAsString" &&
+ it.descriptor == "()Z"
+ }
+ )
+ }
+ }
+ }
+
@Test
fun unsignedMetadata() {
val model = KotlinMetadataModels.objectModel(jsonTypeRef<UnsignedValues>())
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 e63cbf166..eecabbd1c 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
@@ -617,7 +617,7 @@ class KotlinBuiltInCodecsTest {
readName: String,
writeName: String,
) {
- val direct = KotlinUnsignedCodecs.scalar(typeId, false, false) as
DirectUnboxedValueCodec
+ val direct = KotlinUnsignedCodecs.scalar(typeId, false, false, false) as
DirectUnboxedValueCodec
assertEquals(carrier, direct.carrierType())
val readMethod = direct.readCarrierMethod()
assertEquals(readName, readMethod.name)
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 18ca60d32..c8c67a8c1 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
@@ -20,6 +20,7 @@
package org.apache.fory.json.scala
import java.nio.charset.StandardCharsets.UTF_8
+import java.util.concurrent.atomic.AtomicLong
import org.apache.fory.json.ForyJsonException
import org.apache.fory.json.annotation.{JsonIgnore, JsonProperty,
JsonUnwrapped}
@@ -96,6 +97,16 @@ case class NullableRequired(value: String)
case class UserId(value: Int) extends AnyVal
+case class LongId(value: Long) extends AnyVal
+
+case class LongStringValues(
+ aFirst: Long,
+ boxed: java.lang.Long,
+ values: Array[Long],
+ id: LongId,
+ atomic: AtomicLong
+)
+
case class UnitValue(value: Unit)
case class ExplicitNullable(
@@ -144,6 +155,53 @@ case class CodecSlots(
)
class ScalaJsonSuite extends AnyFunSuite {
+ test("long as string") {
+ val value =
+ LongStringValues(
+ Long.MinValue,
+ Long.MaxValue,
+ Array(-1L, 0L, Long.MaxValue),
+ LongId(7L),
+ new AtomicLong(Long.MaxValue)
+ )
+ val list = List(1L, 9007199254740992L)
+ val map = Map("max" -> Long.MaxValue)
+ val optional = Some(9007199254740992L): Option[Long]
+ val listType = ScalaTypeRef[List[Long]]
+ val mapType = ScalaTypeRef[Map[String, Long]]
+ val optionType = ScalaTypeRef[Option[Long]]
+ for (json <- Seq(
+
ForyJsonScala.builder().writeLongAsString(true).withCodegen(false).build(),
+
ForyJsonScala.builder().writeLongAsString(true).withAsyncCompilation(false).build()
+ )) {
+ val encoded = json.toJson(value)
+ assert(encoded.contains("\"aFirst\":\"-9223372036854775808\""), encoded)
+ assert(encoded.contains("\"boxed\":\"9223372036854775807\""), encoded)
+ assert(
+ encoded.contains("\"values\":[\"-1\",\"0\",\"9223372036854775807\"]"),
+ encoded
+ )
+ assert(encoded.contains("\"id\":\"7\""), encoded)
+ assert(encoded.contains("\"atomic\":\"9223372036854775807\""), encoded)
+ assert(new String(json.toJsonBytes(value), UTF_8) == encoded)
+ assert(json.toJson(list, listType) == "[\"1\",\"9007199254740992\"]")
+ assert(json.toJson(map, mapType) == "{\"max\":\"9223372036854775807\"}")
+ assert(json.toJson(optional, optionType) == "\"9007199254740992\"")
+ assert(json.fromJson("[\"1\",9007199254740992]", listType) == list)
+ assert(json.fromJson("{\"max\":\"9223372036854775807\"}", mapType) ==
map)
+ assert(json.fromJson("\"9007199254740992\"", optionType) == optional)
+
+ val decoded = json.fromJson(encoded, classOf[LongStringValues])
+ assert(decoded.aFirst == value.aFirst)
+ assert(decoded.boxed == value.boxed)
+ assert(decoded.values.sameElements(value.values))
+ assert(decoded.id == value.id)
+ assert(decoded.atomic.get() == value.atomic.get())
+ assert(json.fromJson("\"9223372036854775807\"", classOf[Long]) ==
Long.MaxValue)
+ assert(json.fromJson("9223372036854775807", classOf[Long]) ==
Long.MaxValue)
+ }
+ }
+
test("case class collections and recursive option") {
val json = ForyJsonScala.builder().withCodegen(false).build()
val node = Node(1, Some(Node(2, None)))
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]