Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/bson.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export type { CodeExtended } from './code';
export type { DBRefLike } from './db_ref';
export type { Decimal128Extended } from './decimal128';
export type { DoubleExtended } from './double';
export type { EJSONOptions } from './extended_json';
export type { EJSONOptions, EJSONSerializeOptions, EJSONParseOptions } from './extended_json';
export type { Int32Extended } from './int_32';
export type { LongExtended } from './long';
export type { MaxKeyExtended } from './max_key';
Expand Down
43 changes: 30 additions & 13 deletions src/extended_json.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,24 +23,41 @@ import { BSONRegExp } from './regexp';
import { BSONSymbol } from './symbol';
import { Timestamp } from './timestamp';

/** @public */
export type EJSONOptions = {
/** @internal */
type EJSONOptionsBase = {
/**
* Output using the Extended JSON v1 spec
* @defaultValue `false`
*/
legacy?: boolean;
/**
* Enable Extended JSON's `relaxed` mode, which attempts to return native JS types where possible, rather than BSON types
* @defaultValue `false` */
* @defaultValue `false`
*/
relaxed?: boolean;
};

/** @public */
export type EJSONSerializeOptions = EJSONOptionsBase & {
/**
* Omits undefined values from the output instead of converting them to null
* @defaultValue `false`
*/
ignoreUndefined?: boolean;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This option is only relevant to serialization, but EJSONOptions are shared for both serialization and deserialization.

What would you think about creating a separate interface for serialization?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Separated the types. Caused a little more changes than I initially intended to but If it's fine with you I'm glad to help :)

};

/** @public */
export type EJSONParseOptions = EJSONOptionsBase & {
/**
* Enable native bigint support
* @defaultValue `false`
*/
useBigInt64?: boolean;
};

/** @public */
export type EJSONOptions = EJSONSerializeOptions & EJSONParseOptions;

/** @internal */
type BSONType =
| Binary
Expand Down Expand Up @@ -174,12 +191,12 @@ function deserializeValue(value: any, options: EJSONOptions = {}) {
return value;
}

type EJSONSerializeOptions = EJSONOptions & {
type EJSONSerializeInternalOptions = EJSONSerializeOptions & {
seenObjects: { obj: unknown; propertyName: string }[];
};

// eslint-disable-next-line @typescript-eslint/no-explicit-any
function serializeArray(array: any[], options: EJSONSerializeOptions): any[] {
function serializeArray(array: any[], options: EJSONSerializeInternalOptions): any[] {
return array.map((v: unknown, index: number) => {
options.seenObjects.push({ propertyName: `index ${index}`, obj: null });
try {
Expand All @@ -197,7 +214,7 @@ function getISOString(date: Date) {
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
function serializeValue(value: any, options: EJSONSerializeOptions): any {
function serializeValue(value: any, options: EJSONSerializeInternalOptions): any {
if (value instanceof Map || isMap(value)) {
const obj: Record<string, unknown> = Object.create(null);
for (const [k, v] of value) {
Expand Down Expand Up @@ -242,7 +259,7 @@ function serializeValue(value: any, options: EJSONSerializeOptions): any {

if (Array.isArray(value)) return serializeArray(value, options);

if (value === undefined) return null;
if (value === undefined) return options.ignoreUndefined ? undefined : null;

if (value instanceof Date || isDate(value)) {
const dateNum = value.getTime(),
Expand Down Expand Up @@ -326,7 +343,7 @@ const BSON_TYPE_MAPPINGS = {
} as const;

// eslint-disable-next-line @typescript-eslint/no-explicit-any
function serializeDocument(doc: any, options: EJSONSerializeOptions) {
function serializeDocument(doc: any, options: EJSONSerializeInternalOptions) {
if (doc == null || typeof doc !== 'object') throw new BSONError('not an object instance');

const bsontype: BSONType['_bsontype'] = doc._bsontype;
Expand Down Expand Up @@ -410,7 +427,7 @@ function serializeDocument(doc: any, options: EJSONSerializeOptions) {
* ```
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function parse(text: string, options?: EJSONOptions): any {
function parse(text: string, options?: EJSONParseOptions): any {
const ejsonOptions = {
useBigInt64: options?.useBigInt64 ?? false,
relaxed: options?.relaxed ?? true,
Expand Down Expand Up @@ -453,9 +470,9 @@ function stringify(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
value: any,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
replacer?: (number | string)[] | ((this: any, key: string, value: any) => any) | EJSONOptions,
replacer?: (number | string)[] | ((this: any, key: string, value: any) => any) | EJSONSerializeOptions,
space?: string | number,
options?: EJSONOptions
options?: EJSONSerializeOptions
): string {
if (space != null && typeof space === 'object') {
options = space;
Expand All @@ -481,7 +498,7 @@ function stringify(
* @param options - Optional settings passed to the `stringify` function
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function EJSONserialize(value: any, options?: EJSONOptions): Document {
function EJSONserialize(value: any, options?: EJSONSerializeOptions): Document {
options = options || {};
return JSON.parse(stringify(value, options));
}
Expand All @@ -493,7 +510,7 @@ function EJSONserialize(value: any, options?: EJSONOptions): Document {
* @param options - Optional settings passed to the parse method
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function EJSONdeserialize(ejson: Document, options?: EJSONOptions): any {
function EJSONdeserialize(ejson: Document, options?: EJSONParseOptions): any {
options = options || {};
return parse(JSON.stringify(ejson), options);
}
Expand Down
114 changes: 114 additions & 0 deletions test/node/extended_json.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -574,6 +574,120 @@ describe('Extended JSON', function () {
expect(result).to.deep.equal({ a: 1 });
});

describe('ignoreUndefined option', () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The changes you've made implicitly add support for ignoreUndefined to our EJSON.serialize function too. Could you add a set of tests like these tests for the EJSON.serialize function as well?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@baileympearson Added tests for EJSON.serialize

it('should convert undefined to null by default', () => {
const doc = { a: 1, b: undefined, c: 'test' };
const serialized = EJSON.stringify(doc);
expect(serialized).to.equal('{"a":1,"b":null,"c":"test"}');
});

it('should omit undefined values when ignoreUndefined is true', () => {
const doc = { a: 1, b: undefined, c: 'test' };
const serialized = EJSON.stringify(doc, { ignoreUndefined: true });
expect(serialized).to.equal('{"a":1,"c":"test"}');
});

it('should handle nested undefined values with ignoreUndefined: true', () => {
const doc = { a: 1, nested: { b: undefined, c: 2 }, d: 'test' };
const serialized = EJSON.stringify(doc, { ignoreUndefined: true });
expect(serialized).to.equal('{"a":1,"nested":{"c":2},"d":"test"}');
});

it('should handle nested undefined values without ignoreUndefined (default behavior)', () => {
const doc = { a: 1, nested: { b: undefined, c: 2 }, d: 'test' };
const serialized = EJSON.stringify(doc);
expect(serialized).to.equal('{"a":1,"nested":{"b":null,"c":2},"d":"test"}');
});

it('should handle undefined in arrays with ignoreUndefined: true', () => {
const doc = { arr: [1, undefined, 3] };
const serialized = EJSON.stringify(doc, { ignoreUndefined: true });
// JSON.stringify converts undefined array elements to null
expect(serialized).to.equal('{"arr":[1,null,3]}');
});

it('should handle undefined in arrays without ignoreUndefined (default behavior)', () => {
const doc = { arr: [1, undefined, 3] };
const serialized = EJSON.stringify(doc);
expect(serialized).to.equal('{"arr":[1,null,3]}');
});

it('should handle object with all undefined values with ignoreUndefined: true', () => {
const doc = { a: undefined, b: undefined };
const serialized = EJSON.stringify(doc, { ignoreUndefined: true });
expect(serialized).to.equal('{}');
});

it('should work with other options like relaxed', () => {
const doc = { a: new Int32(10), b: undefined, c: new Double(3.14) };
const serialized = EJSON.stringify(doc, { ignoreUndefined: true, relaxed: false });
expect(serialized).to.equal('{"a":{"$numberInt":"10"},"c":{"$numberDouble":"3.14"}}');
});

it('should work with replacer function', () => {
const doc = { a: 1, b: undefined, c: 2 };
const replacer = (key: string, value: unknown) => (key === 'a' ? 100 : value);
const serialized = EJSON.stringify(doc, replacer, 0, { ignoreUndefined: true });
expect(serialized).to.equal('{"a":100,"c":2}');
});

it('should work with space parameter', () => {
const doc = { a: 1, b: undefined };
const serialized = EJSON.stringify(doc, undefined, 2, { ignoreUndefined: true });
expect(serialized).to.equal('{\n "a": 1\n}');
});
});

describe('ignoreUndefined option in EJSON.serialize', () => {
it('should convert undefined to null by default', () => {
const doc = { a: 1, b: undefined, c: 'test' };
const serialized = EJSON.serialize(doc);
expect(serialized).to.deep.equal({ a: 1, b: null, c: 'test' });
});

it('should omit undefined values when ignoreUndefined is true', () => {
const doc = { a: 1, b: undefined, c: 'test' };
const serialized = EJSON.serialize(doc, { ignoreUndefined: true });
expect(serialized).to.deep.equal({ a: 1, c: 'test' });
});

it('should handle nested undefined values with ignoreUndefined: true', () => {
const doc = { a: 1, nested: { b: undefined, c: 2 }, d: 'test' };
const serialized = EJSON.serialize(doc, { ignoreUndefined: true });
expect(serialized).to.deep.equal({ a: 1, nested: { c: 2 }, d: 'test' });
});

it('should handle nested undefined values without ignoreUndefined (default behavior)', () => {
const doc = { a: 1, nested: { b: undefined, c: 2 }, d: 'test' };
const serialized = EJSON.serialize(doc);
expect(serialized).to.deep.equal({ a: 1, nested: { b: null, c: 2 }, d: 'test' });
});

it('should handle undefined in arrays with ignoreUndefined: true', () => {
const doc = { arr: [1, undefined, 3] };
const serialized = EJSON.serialize(doc, { ignoreUndefined: true });
expect(serialized).to.deep.equal({ arr: [1, null, 3] });
});

it('should handle undefined in arrays without ignoreUndefined (default behavior)', () => {
const doc = { arr: [1, undefined, 3] };
const serialized = EJSON.serialize(doc);
expect(serialized).to.deep.equal({ arr: [1, null, 3] });
});

it('should handle object with all undefined values with ignoreUndefined: true', () => {
const doc = { a: undefined, b: undefined };
const serialized = EJSON.serialize(doc, { ignoreUndefined: true });
expect(serialized).to.deep.equal({});
});

it('should work with relaxed: false option', () => {
const doc = { a: new Int32(10), b: undefined, c: new Double(3.14) };
const serialized = EJSON.serialize(doc, { ignoreUndefined: true, relaxed: false });
expect(serialized).to.deep.equal({ a: { $numberInt: '10' }, c: { $numberDouble: '3.14' } });
});
});

it(`throws if Symbol.for('@@mdb.bson.version') is the wrong version in EJSON.stringify`, () => {
expect(() =>
EJSON.stringify({
Expand Down