From 7e18fb39d607300f6cee9e780c9e999ab9da11ac Mon Sep 17 00:00:00 2001 From: ayush00git Date: Tue, 1 Sep 2026 23:59:33 +0530 Subject: [PATCH 1/2] fix(javascript): honor declared element types for root container registration Registering a root Type.list/set/map with declared element types returned the internal any-typed container serializer, silently discarding the declared generics (e.g. declared float32 elements kept float64 dynamic encoding). Generate a dedicated serializer for such registrations, bind it to the returned root deserializer, and keep it out of the type-id keyed registry so dynamic container dispatch stays untouched. --- javascript/packages/core/lib/fory.ts | 17 ++++++++++----- javascript/packages/core/lib/gen/index.ts | 8 ++++++- javascript/packages/core/lib/typeInfo.ts | 26 +++++++++++++++++++++++ javascript/test/array.test.ts | 19 +++++++++++++++++ javascript/test/map.test.ts | 14 ++++++++++++ 5 files changed, 78 insertions(+), 6 deletions(-) diff --git a/javascript/packages/core/lib/fory.ts b/javascript/packages/core/lib/fory.ts index 8eff8df042..04cd1fd6e4 100644 --- a/javascript/packages/core/lib/fory.ts +++ b/javascript/packages/core/lib/fory.ts @@ -27,7 +27,7 @@ import { TypeId, CustomSerializer, } from "./type"; -import { InputType, ResultType, TypeInfo } from "./typeInfo"; +import { containerDeclaresElementTypes, InputType, ResultType, TypeInfo } from "./typeInfo"; import { Gen } from "./gen"; import { PlatformBuffer } from "./platformBuffer"; import { ReadContext, WriteContext } from "./context"; @@ -163,7 +163,12 @@ export default class Fory { serializer = new Gen(this.typeResolver, { customSerializer, }).generateSerializer(typeInfo); - this.typeResolver.registerSerializer(typeInfo, serializer); + if (!containerDeclaresElementTypes(typeInfo)) { + // A declared-element container serializer is bound to this + // registration only; publishing it under the bare container type id + // would replace the dynamic container serializer. + this.typeResolver.registerSerializer(typeInfo, serializer); + } } return { serializer, @@ -223,9 +228,11 @@ export default class Fory { } const readContext = this.readContext; const reader = readContext.reader; - const rootSerializer = TypeId.polymorphicType(serializer.getTypeId()) - ? serializer - : this.anySerializer; + const rootSerializer = + TypeId.polymorphicType(serializer.getTypeId()) || + containerDeclaresElementTypes(serializer.getTypeInfo()) + ? serializer + : this.anySerializer; const rootHeader = ConfigFlags.isCrossLanguageFlag; rootDeserializer = (bytes: Uint8Array) => { readContext.reset(bytes); diff --git a/javascript/packages/core/lib/gen/index.ts b/javascript/packages/core/lib/gen/index.ts index 9e62446722..60049f440a 100644 --- a/javascript/packages/core/lib/gen/index.ts +++ b/javascript/packages/core/lib/gen/index.ts @@ -18,7 +18,7 @@ */ import { TypeId, Serializer } from "../type"; -import { TypeInfo } from "../typeInfo"; +import { containerDeclaresElementTypes, TypeInfo } from "../typeInfo"; import { CodegenRegistry } from "./router"; import { CodecBuilder } from "./builder"; import { Scope } from "./scope"; @@ -175,6 +175,12 @@ export class Gen { generateSerializer(typeInfo: TypeInfo) { this.traversalContainer(typeInfo); + if (containerDeclaresElementTypes(typeInfo)) { + // The type-id keyed registry only holds the dynamic container + // serializer; a container with declared element types gets a dedicated + // serializer for this registration. + return this.generate(typeInfo); + } const serializer = this.typeResolver.getSerializerByTypeInfo(typeInfo); if (serializer?._initialized) { return serializer; diff --git a/javascript/packages/core/lib/typeInfo.ts b/javascript/packages/core/lib/typeInfo.ts index 809d7efcbc..bd1c63e3f9 100644 --- a/javascript/packages/core/lib/typeInfo.ts +++ b/javascript/packages/core/lib/typeInfo.ts @@ -26,6 +26,32 @@ import { Decimal } from "./types/decimal"; const targetFields = new WeakMap any, { [key: string]: TypeInfo }>(); export const MAX_FIELD_ID = (1 << 29) - 1; +/** + * Whether this container TypeInfo declares concrete element types instead of + * dynamic `any` elements. Such a TypeInfo is a usage schema for one + * registration: it needs its own generated serializer and must not replace + * the dynamic container serializer in the type-id keyed registry. + */ +export function containerDeclaresElementTypes(typeInfo: TypeInfo): boolean { + if (typeInfo.typeId === TypeId.LIST) { + const inner = typeInfo.options?.inner; + return inner !== undefined && inner.typeId !== TypeId.UNKNOWN; + } + if (typeInfo.typeId === TypeId.SET) { + const inner = typeInfo.options?.key; + return inner !== undefined && inner.typeId !== TypeId.UNKNOWN; + } + if (typeInfo.typeId === TypeId.MAP) { + const key = typeInfo.options?.key; + const value = typeInfo.options?.value; + return ( + (key !== undefined && key.typeId !== TypeId.UNKNOWN) || + (value !== undefined && value.typeId !== TypeId.UNKNOWN) + ); + } + return false; +} + export function checkFieldId(fieldId: number) { if (Number.isFinite(fieldId) && fieldId < 0) { throw new Error("field id must be non-negative"); diff --git a/javascript/test/array.test.ts b/javascript/test/array.test.ts index 989bbf7f8b..54444303e8 100644 --- a/javascript/test/array.test.ts +++ b/javascript/test/array.test.ts @@ -77,6 +77,25 @@ describe("array", () => { expect(deserialize(serialize({ c: [o, o] }))).toEqual({ c: [o, o] }); }); + test("should root list use declared element type", () => { + // A root Type.list(...) registration previously fell back to the internal + // any-typed list serializer, silently discarding declared element types: + // declared float32 must narrow, while dynamic dispatch keeps float64. + const fory = new Fory({ compatible: false }); + const { serialize, deserialize } = fory.register(Type.list(Type.float32())); + expect(deserialize(serialize([0.1]))).toEqual([Math.fround(0.1)]); + + // The dynamic list serializer must stay untouched by the registration. + expect(fory.deserialize(fory.serialize([0.1, "a"]))).toEqual([0.1, "a"]); + }); + + test("should root set use declared element type", () => { + const fory = new Fory({ compatible: false }); + const { serialize, deserialize } = fory.register(Type.set(Type.float32())); + expect(deserialize(serialize(new Set([0.1])))).toEqual(new Set([Math.fround(0.1)])); + expect(fory.deserialize(fory.serialize(new Set([0.1, "a"])))).toEqual(new Set([0.1, "a"])); + }); + test("preserves a self-reference in a dynamic list", () => { const fory = new Fory({ compatible: false, ref: true }); const value: any[] = []; diff --git a/javascript/test/map.test.ts b/javascript/test/map.test.ts index 367170aff9..b3b5828923 100644 --- a/javascript/test/map.test.ts +++ b/javascript/test/map.test.ts @@ -97,6 +97,20 @@ describe("map", () => { }); }); + test("should root map use declared key and value types", () => { + // A root Type.map(...) registration previously fell back to the internal + // any-typed map serializer, silently discarding declared key/value types: + // declared float32 must narrow, while dynamic dispatch keeps float64. + const fory = new Fory({ compatible: false }); + const { serialize, deserialize } = fory.register(Type.map(Type.string(), Type.float32())); + expect(deserialize(serialize(new Map([["a", 0.1]])))).toEqual( + new Map([["a", Math.fround(0.1)]]), + ); + + // The dynamic map serializer must stay untouched by the registration. + expect(fory.deserialize(fory.serialize(new Map([[1, "x"]])))).toEqual(new Map([[1, "x"]])); + }); + test("preserves shared dynamic map entries", () => { const fory = new Fory({ compatible: false, ref: true }); @Type.struct(301, { From a8ef87fd984fc3dd83bff4afc8efe64ae05527d3 Mon Sep 17 00:00:00 2001 From: ayush00git Date: Thu, 3 Sep 2026 18:11:16 +0530 Subject: [PATCH 2/2] fix(javascript): bind declared containers to ext codecs registered later A declared container generated before its extension codec captured undefined, because traversalContainer only created forward placeholders for struct types. Serialization then failed at the ext serializer's writeTypeInfo. Register the same forward placeholder for ext types so the generated container binds to the placeholder object that the later codec registration fills, preserving free registration order before the first root operation. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01HDxaC93cfnTxCciswW1NHM --- javascript/packages/core/lib/gen/index.ts | 13 ++++++---- javascript/test/array.test.ts | 31 +++++++++++++++++++++++ 2 files changed, 39 insertions(+), 5 deletions(-) diff --git a/javascript/packages/core/lib/gen/index.ts b/javascript/packages/core/lib/gen/index.ts index 60049f440a..9b9a4f8416 100644 --- a/javascript/packages/core/lib/gen/index.ts +++ b/javascript/packages/core/lib/gen/index.ts @@ -139,11 +139,14 @@ export class Gen { this.traversalContainer(x); }); this.register(typeInfo, this.generate(typeInfo)); - } else if (!this.isRegistered(typeInfo) && TypeId.structType(typeInfo.typeId)) { - // Forward reference to a struct type not yet fully defined — register a - // placeholder so that serializer factories can capture the object - // reference. The placeholder will be filled in via Object.assign - // when the real serializer is generated later. + } else if ( + !this.isRegistered(typeInfo) && + (TypeId.structType(typeInfo.typeId) || TypeId.extType(typeInfo.typeId)) + ) { + // Forward reference to a struct or ext type not yet fully defined — + // register a placeholder so that serializer factories can capture the + // object reference. The placeholder will be filled in via + // Object.assign when the real serializer is generated later. this.register(typeInfo); } else if (TypeId.enumType(typeInfo.typeId) && !this.isRegistered(typeInfo)) { this.register(typeInfo, this.generate(typeInfo)); diff --git a/javascript/test/array.test.ts b/javascript/test/array.test.ts index 54444303e8..9073fb11aa 100644 --- a/javascript/test/array.test.ts +++ b/javascript/test/array.test.ts @@ -96,6 +96,37 @@ describe("array", () => { expect(fory.deserialize(fory.serialize(new Set([0.1, "a"])))).toEqual(new Set([0.1, "a"])); }); + test("should root container registered before its ext codec work", () => { + // Registration order is free before the first root operation: the + // container's forward ext placeholder must be filled when the extension + // codec registers later, so the generated serializer binds to the + // completed codec instead of capturing undefined. + class ListedExtension { + constructor(public id = 0) {} + } + Type.ext(921)(ListedExtension); + const extCodec = { + write(context: any, value: ListedExtension) { + context.writeUint8(value.id); + }, + read(context: any, result: ListedExtension) { + result.id = context.readUint8(); + }, + }; + + const listFory = new Fory({ compatible: false }); + const list = listFory.register(Type.list(Type.ext(921))); + listFory.register(ListedExtension, extCodec); + const listResult = list.deserialize(list.serialize([new ListedExtension(7)])); + expect(listResult).toEqual([new ListedExtension(7)]); + + const setFory = new Fory({ compatible: false }); + const set = setFory.register(Type.set(Type.ext(921))); + setFory.register(ListedExtension, extCodec); + const setResult = set.deserialize(set.serialize(new Set([new ListedExtension(9)]))); + expect(setResult).toEqual(new Set([new ListedExtension(9)])); + }); + test("preserves a self-reference in a dynamic list", () => { const fory = new Fory({ compatible: false, ref: true }); const value: any[] = [];