From 19a4e45e7bece3582f64a02e69a596f732948000 Mon Sep 17 00:00:00 2001 From: Eduardo Eidelwein Berlitz Date: Thu, 12 Jun 2025 11:54:47 +0200 Subject: [PATCH 1/2] feat: add undefinedAsNil option to Encoder --- src/Encoder.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/Encoder.ts b/src/Encoder.ts index 43fcd8f..499d7c2 100644 --- a/src/Encoder.ts +++ b/src/Encoder.ts @@ -61,6 +61,15 @@ export type EncoderOptions = Partial< */ ignoreUndefined: boolean; + /** + * If `true`, an object property with `undefined` value are encoded as `nil`, same as null. + * e.g. `{ foo: undefined }` will be encoded as `{ foo: nil }`. + * This flag has no effect if `ignoreUndefined` is `true`. + * + * Defaults to `true`. + */ + undefinedAsNil: boolean; + /** * If `true`, integer numbers are encoded as floating point numbers, * with the `forceFloat32` option taken into account. @@ -82,6 +91,7 @@ export class Encoder { private readonly forceFloat32: boolean; private readonly ignoreUndefined: boolean; private readonly forceIntegerToFloat: boolean; + private readonly undefinedAsNil: boolean; private pos: number; private view: DataView; @@ -99,6 +109,7 @@ export class Encoder { this.sortKeys = options?.sortKeys ?? false; this.forceFloat32 = options?.forceFloat32 ?? false; this.ignoreUndefined = options?.ignoreUndefined ?? false; + this.undefinedAsNil = options?.undefinedAsNil ?? true; this.forceIntegerToFloat = options?.forceIntegerToFloat ?? false; this.pos = 0; @@ -174,7 +185,9 @@ export class Encoder { throw new Error(`Too deep objects in depth ${depth}`); } - if (object == null) { + if (object === null) { + this.encodeNil(); + } else if (object === undefined && this.undefinedAsNil) { this.encodeNil(); } else if (typeof object === "boolean") { this.encodeBoolean(object); From 2f2e3c7d07967c35c46fb12193f866bc795934b6 Mon Sep 17 00:00:00 2001 From: Eduardo Eidelwein Berlitz Date: Thu, 12 Jun 2025 14:51:46 +0200 Subject: [PATCH 2/2] Update readme and add tests --- README.md | 1 + test/encode.test.ts | 36 +++++++++++++++++++++++++++++++++++- 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index c0f4dc0..87811a6 100644 --- a/README.md +++ b/README.md @@ -122,6 +122,7 @@ console.log(buffer); | forceFloat32 | boolean | false | | forceIntegerToFloat | boolean | false | | ignoreUndefined | boolean | false | +| undefinedAsNil | boolean | true | ### `decode(buffer: ArrayLike | BufferSource, options?: DecoderOptions): unknown` diff --git a/test/encode.test.ts b/test/encode.test.ts index f1b6ffe..0efba93 100644 --- a/test/encode.test.ts +++ b/test/encode.test.ts @@ -1,5 +1,5 @@ import assert from "assert"; -import { encode, decode } from "../src/index.ts"; +import { encode, decode, ExtensionCodec } from "../src/index.ts"; describe("encode", () => { context("sortKeys", () => { @@ -66,6 +66,40 @@ describe("encode", () => { }); }); + context("undefinedAsNil", () => { + const extensionCodec = new ExtensionCodec(); + extensionCodec.register({ + type: 0, + encode: (value) => (value === undefined ? new Uint8Array([0]) : null), + decode: () => undefined, + }); + + it("encodes { foo: undefined } as null by default", () => { + assert.deepStrictEqual(decode(encode({ foo: undefined, bar: 42 })), { foo: null, bar: 42 }); + }); + + it("encodes { foo: undefined } as undefined with `undefinedAsNil: false`", () => { + assert.deepStrictEqual( + decode(encode({ foo: undefined, bar: 42 }, { extensionCodec, ignoreUndefined: false, undefinedAsNil: false }), { + extensionCodec, + }), + { + foo: undefined, + bar: 42, + }, + ); + }); + + it("encodes { foo: undefined } to { foo: null } with `undefinedAsNil: true`", () => { + assert.deepStrictEqual( + decode(encode({ foo: undefined, bar: 42 }, { extensionCodec, ignoreUndefined: false, undefinedAsNil: true }), { + extensionCodec, + }), + { foo: null, bar: 42 }, + ); + }); + }); + context("ArrayBuffer as buffer", () => { const buffer = encode([1, 2, 3]); const arrayBuffer = buffer.buffer.slice(buffer.byteOffset, buffer.byteLength);