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 fb002e216 feat(js): add schema-based per-field nullable support for
xlang (#3100)
fb002e216 is described below
commit fb002e216bb35cdb3fe5878b59017fb890796e76
Author: Harsh Patel <[email protected]>
AuthorDate: Sun Jan 4 08:05:23 2026 +0530
feat(js): add schema-based per-field nullable support for xlang (#3100)
## Why?
JavaScript xlang schema-based struct serialization currently treats all
fields as nullable and always writes a per-field null flag.
This introduces unnecessary overhead when a field is known to be
non-nullable by schema design.
## What does this PR do?
- Adds schema-based per-field `nullable` support for JS xlang struct
serialization.
- Preserves backward compatibility by treating fields as nullable by
default.
- When `nullable: false` is explicitly specified:
- Skips writing the per-field null flag.
- Throws a clear runtime error if the field value is `null` or
`undefined` (includes field name).
- Extends `StructTypeInfo` field schema typing to allow `nullable?:
boolean`.
- Adds a minimal unit test verifying:
- Non-nullable fields throw on `null`.
- Fields without `nullable` keep existing behavior.
## Related issues
## Does this PR introduce any user-facing change?
Yes.
This PR introduces an **opt-in** schema-level configuration (`nullable:
false`) for JS xlang serialization.
Existing schemas are unaffected unless `nullable: false` is explicitly
set.
- [x] Does this PR introduce any public API change?
- [x] Does this PR introduce any binary protocol compatibility change?
## Benchmark
This change is opt-in and only affects schema-based fields explicitly
marked as `nullable: false`.
No performance regression is expected for existing schemas.
---------
Co-authored-by: Harsh Patel <[email protected]>
---
javascript/packages/fory/lib/gen/struct.ts | 6 +++-
javascript/packages/fory/lib/typeInfo.ts | 2 +-
javascript/test/protocol/struct.test.ts | 48 +++++++++++++++++++++++++++++-
3 files changed, 53 insertions(+), 3 deletions(-)
diff --git a/javascript/packages/fory/lib/gen/struct.ts
b/javascript/packages/fory/lib/gen/struct.ts
index 43e93b326..1ef250829 100644
--- a/javascript/packages/fory/lib/gen/struct.ts
+++ b/javascript/packages/fory/lib/gen/struct.ts
@@ -76,7 +76,11 @@ class StructSerializerGenerator extends
BaseSerializerGenerator {
throw new Error(`${inner.type} generator not exists`);
}
const innerGenerator = new InnerGeneratorClass(inner, this.builder,
this.scope);
- return
innerGenerator.toWriteEmbed(`${accessor}${CodecBuilder.safePropAccessor(key)}`);
+
+ const fieldAccessor =
`${accessor}${CodecBuilder.safePropAccessor(key)}`;
+ return (inner as any).nullable === false
+ ? `if (${fieldAccessor} === null || ${fieldAccessor} === undefined)
{ throw new Error("Field '${CodecBuilder.replaceBackslashAndQuote(key)}' is not
nullable"); }\n${innerGenerator.toWriteEmbed(fieldAccessor, true)}`
+ : innerGenerator.toWriteEmbed(fieldAccessor);
}).join(";\n")}
`;
}
diff --git a/javascript/packages/fory/lib/typeInfo.ts
b/javascript/packages/fory/lib/typeInfo.ts
index 04af40392..d29c1b6f8 100644
--- a/javascript/packages/fory/lib/typeInfo.ts
+++ b/javascript/packages/fory/lib/typeInfo.ts
@@ -205,7 +205,7 @@ export class TypeInfo<T = unknown> extends
ExtensibleFunction {
export interface StructTypeInfo extends TypeInfo {
options: {
- props?: { [key: string]: TypeInfo };
+ props?: { [key: string]: TypeInfo & { nullable?: boolean } };
withConstructor?: boolean;
};
}
diff --git a/javascript/test/protocol/struct.test.ts
b/javascript/test/protocol/struct.test.ts
index 2b5633fdd..290e1effb 100644
--- a/javascript/test/protocol/struct.test.ts
+++ b/javascript/test/protocol/struct.test.ts
@@ -23,7 +23,7 @@ import { describe, expect, test } from '@jest/globals';
describe('protocol', () => {
test('should polymorphic work', () => {
-
+
const fory = new Fory({ refTracking: true });
const { serialize, deserialize } =
fory.registerSerializer(Type.struct({
typeName: "example.foo"
@@ -45,6 +45,52 @@ describe('protocol', () => {
const result = deserialize(bf);
expect(result).toEqual(obj);
});
+
+ test('should enforce nullable flag for schema-based structs', () => {
+ const fory = new Fory();
+
+ // 1) nullable: false => null must throw
+ const nonNullable = Type.struct({
+ typeName: "example.nonNullable"
+ }, {
+ a: Object.assign(Type.string(), { nullable: false }),
+ });
+ const nonNullableSer = fory.registerSerializer(nonNullable);
+ expect(() => nonNullableSer.serialize({ a: null })).toThrow(/Field 'a'
is not nullable/);
+
+ // 2) nullable not specified => keep old behavior (null allowed)
+ const nullableUnspecified = Type.struct({
+ typeName: "example.nullableUnspecified"
+ }, {
+ a: Type.string(),
+ });
+ const { serialize, deserialize } =
fory.registerSerializer(nullableUnspecified);
+ expect(deserialize(serialize({ a: null }))).toEqual({ a: null });
+ });
+
+ test('should enforce nullable flag in schema-consistent mode', () => {
+ const fory = new Fory({ mode: 'SCHEMA_CONSISTENT' as any });
+
+ const schema = Type.struct(
+ { typeName: 'example.schemaConsistentNullable' },
+ {
+ a: Object.assign(Type.string(), { nullable: false }),
+ b: Type.string(),
+ }
+ );
+
+ const { serialize, deserialize } = fory.registerSerializer(schema);
+
+ // non-nullable field must throw
+ expect(() => serialize({ a: null, b: 'ok' }))
+ .toThrow(/Field 'a' is not nullable/);
+
+ // unspecified nullable field keeps old behavior
+ expect(deserialize(serialize({ a: 'ok', b: null })))
+ .toEqual({ a: undefined, b: null });
+ });
});
+
+
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]