diff --git a/README.md b/README.md index c1144eb..5ccf58f 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,7 @@ for your data management needs. Explore the [Sequelize support](#sequelize-suppo + [Global Serializer](#global-serializer) + [Field-Specific Serializer](#field-specific-serializer) * [Contextual Field-Specific Serializer](#contextual-field-specific-serializer) + * [Limit Serialization Depth](#limit-serialization-depth) - [Circular References](#circular-references) - [Sequelize support](#sequelize-support) - [Let's Contribute Together!](#lets-contribute-together) @@ -229,6 +230,40 @@ console.log(jsonthis.toJson(user, {context: {maskChar: "-"}})); // { id: 1, email: 'j------e@gmail.com' } ``` +### Limit Serialization Depth + +You can limit the depth of serialization by setting the `maxDepth` option at global level in `JsonthisOptions` +at construction time: + +```typescript +class User { + id: number; + name: string; + friend?: User; + + constructor(id: number, name: string) { + this.id = id; + this.name = name; + } +} + +const user = new User(1, "John"); +user.friend = new User(2, "Jane"); +user.friend.friend = new User(3, "Bob"); + +const jsonthis = new Jsonthis({maxDepth: 1}); +console.log(jsonthis.toJson(user)); +// { id: 1, name: 'John', friend: { id: 2, name: 'Jane' } } +``` + +You can also set the `maxDepth` option at the method level in `ToJsonOptions`: + +```typescript +const jsonthis = new Jsonthis(); +console.log(jsonthis.toJson(user, {maxDepth: 1})); +// { id: 1, name: 'John', friend: { id: 2, name: 'Jane' } } +``` + ## Circular References Jsonthis can detect circular references out of the box. When serializing an object with circular references, the default diff --git a/src/jsonthis.test.ts b/src/jsonthis.test.ts index 29c01c0..c8c8da6 100644 --- a/src/jsonthis.test.ts +++ b/src/jsonthis.test.ts @@ -577,5 +577,60 @@ describe("Jsonthis class", () => { }); }); }); + + describe("with maxDepth option", () => { + class User { + id: number; + friend?: User; + + constructor(id: number, friend?: User) { + this.id = id; + if (friend) this.friend = friend; + } + } + + const user = new User(1, new User(2, new User(3, new User(4)))); + + it("should serialize to unlimited depth by default", () => { + const jsonthis = new Jsonthis(); + expect(jsonthis.toJson(user)).toStrictEqual({ + id: 1, + friend: { + id: 2, + friend: { + id: 3, + friend: {id: 4} + } + } + }); + }); + + it("should stop serialization to global maxDepth", () => { + const jsonthis = new Jsonthis({maxDepth: 2}); + expect(jsonthis.toJson(user)).toStrictEqual({ + id: 1, + friend: { + id: 2, + friend: {id: 3} + } + }); + }); + + it("should stop serialization to field's maxDepth", () => { + const jsonthis = new Jsonthis(); + expect(jsonthis.toJson(user, {maxDepth: 1})).toStrictEqual({ + id: 1, + friend: {id: 2} + }); + }); + + it("should stop serialization to field's maxDepth over global maxDepth", () => { + const jsonthis = new Jsonthis({maxDepth: 2}); + expect(jsonthis.toJson(user, {maxDepth: 1})).toStrictEqual({ + id: 1, + friend: {id: 2} + }); + }); + }); }); }); \ No newline at end of file diff --git a/src/jsonthis.ts b/src/jsonthis.ts index f95286b..0306411 100644 --- a/src/jsonthis.ts +++ b/src/jsonthis.ts @@ -22,6 +22,7 @@ export type JsonthisOptions = { case?: "camel" | "snake" | "pascal"; // The case to use for field names, default is to keep field name as is. sequelize?: Sequelize; // Install Jsonthis to this Sequelize instance. circularReferenceSerializer?: JsonTraversalFn; // The custom serializer function for circular references, default it to throw an error. + maxDepth?: number; // The maximum depth to traverse the object, default is unlimited. } /** @@ -29,6 +30,7 @@ export type JsonthisOptions = { */ export type ToJsonOptions = { context?: any; // The user-defined context object to pass to the serializers. + maxDepth?: number; // The maximum depth to traverse the object, default is the global maxDepth in JsonthisOptions. } export class CircularReferenceError extends Error { @@ -149,6 +151,10 @@ export class Jsonthis { try { state.visited.dive(); + const maxDepth = options?.maxDepth || this.options.maxDepth || Infinity; + if (state.visited.depth > maxDepth) + return undefined; + // Check for circular references if (state.visited.visit(value)) { if (this.options.circularReferenceSerializer) diff --git a/src/schema.ts b/src/schema.ts index 7e1f2b2..f25e8ae 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -28,6 +28,10 @@ export class VisitMap { private depths: Array>> = [new Map>()] private currentDepth: number = 0 + public get depth(): number { + return this.currentDepth; + } + private has(value: any): boolean { for (const map of this.depths) { const values = map.get(value);