Skip to content

Jsonthis is the perfect tool to convert your models to JSON objects

License

Notifications You must be signed in to change notification settings

davidecaroselli/jsonthis

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

50 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

🚀 Jsonthis!

npm version npm download github prs npm license

Jsonthis! is a versatile TypeScript library designed to effortlessly convert your models into JSON objects. It offers extensive support for custom property serializers, conditional property visibility, and more.

Jsonthis! seamlessly integrates with the Sequelize ORM library, making it an ideal companion for your data management needs. Explore the Sequelize support section for detailed instructions.

Table of Contents

Installation

To install Jsonthis!, simply run:

npm install jsonthis

Getting Started

Getting started with Jsonthis! is quick and straightforward. Here's a simple example to get you going:

import {JsonField, Jsonthis} from "jsonthis";

class User {
    id: number;
    email: string;
    @JsonField(false)  // visible=false - the "password" property will not be included in the JSON output
    password: string;
    registeredAt: Date = new Date();

    constructor(id: number, email: string, password: string) {
        this.id = id;
        this.email = email;
        this.password = password;
    }

    declare toJSON: () => any;
}

const user = new User(1, "[email protected]", "s3cret");

const jsonthis = new Jsonthis({models: [User]});
console.log(user.toJSON());
// {
//   id: 1,
//   email: '[email protected]',
//   registeredAt: 2024-04-27T17:03:52.158Z
// }

The @JsonField decorator empowers you to fine-tune the serialization process of your properties with Jsonthis!: you can define custom serializers, change property visibility, and more.

JSON Serialization

Jsonthis! offer a toJson(target, options?) method, as well as the toJSON() method on your classes via the models options in the constructor. The first allows for more flexibility and customization (such as conditional-serialization - see Conditional Visibility for more details), while the latter is a more straightforward approach that makes your classes compatible with JSON.stringify().

class User {
    // (...) properties and methods

    // This prevent TypeScript from throwing an error when calling toJSON() on the User class
    declare toJSON: () => any;
}

const jsonthis = new Jsonthis({
    models: [User]  // This will instruct Jsonthis! to implement the toJSON() method on the User class
});

You can then use the toJSON() method on your class instances, or stringify them directly with JSON.stringify():

const user = new User();
console.log(user.toJSON());  // This will return a JSON-compatible object
console.log(JSON.stringify(user));  // This will return the JSON string of the object

Alternatively, you can use the toJson() method on the Jsonthis! instance, which allows for more customization:

const user = new User();
const jsonUser = jsonthis.toJson(user, /* options */);
console.log(jsonUser);  // The object resulting from the serialization process
console.log(JSON.stringify(jsonUser));  // This will return the JSON string of the object

Change Property Visibility

You can hide a property from the JSON output by setting the visible option to false. You can achieve this by passing false to the @JsonField decorator directly or by using the JsonFieldOptions object:

class User {
    // ...
    @JsonField({visible: false})  // This has the same effect as @JsonField(false)
    password: string;
    // ...
}

Conditional Visibility

Jsonthis! supports conditional property visibility based on a user-defined context. This allows you to dynamically show or hide properties as needed.

In the following example, the email property is only visible if the email owner is requesting it:

function showEmailOnlyToOwner(jsonthis: Jsonthis, state: JsonTraversalState, value: string, options?: ToJsonOptions): boolean {
    return options?.context?.callerId === (state.parent as User)?.id;
}

class User {
    id: number;
    @JsonField({visible: showEmailOnlyToOwner})
    email: string;
    friend?: User;

    constructor(id: number, email: string) {
        this.id = id;
        this.email = email;
    }

    declare toJSON: () => any;
}

const user = new User(1, "[email protected]");

const jsonthis = new Jsonthis({models: [User]});
console.log(jsonthis.toJson(user, {context: {callerId: 1}}));
// { id: 1, email: '[email protected]' }

console.log(jsonthis.toJson(user, {context: {callerId: 2}}));
// { id: 1 }

This also works with nested objects:

const user = new User(1, "[email protected]");
user.friend = new User(2, "[email protected]");

const jsonthis = new Jsonthis({models: [User]});
console.log(jsonthis.toJson(user, {context: {callerId: 1}}));
// { id: 1, email: '[email protected]', friend: { id: 2 } }

console.log(jsonthis.toJson(user, {context: {callerId: 2}}));
// { id: 1, friend: { id: 2, email: '[email protected]' } }

Customizing Serialization

Change Property Name Casing

Jsonthis! allows you to enforce specific casing for property names in the JSON output. By default, Jsonthis! uses whatever casing you use in your TypeScript code, but you can change it to camelCase, snake_case, or PascalCase:

class User {
    id: number = 123;
    user_name: string = "john-doe";
    registeredAt: Date = new Date();

    declare toJSON: () => any;
}

const user = new User();

new Jsonthis({models: [User]});
console.log(user.toJSON());
// { id: 123, user_name: 'john-doe', registeredAt: 2024-04-27T17:03:52.158Z }

new Jsonthis({case: "camel", models: [User]});
console.log(user.toJSON());
// { id: 123, userName: 'john-doe', registeredAt: 2024-04-27T17:03:52.158Z }

new Jsonthis({case: "snake", models: [User]});
console.log(user.toJSON());
// { id: 123, user_name: 'john-doe', registered_at: 2024-04-27T17:03:52.158Z }

new Jsonthis({case: "pascal", models: [User]});
console.log(user.toJSON());
// { Id: 123, UserName: 'john-doe', RegisteredAt: 2024-04-27T17:03:52.158Z }

Custom serializers

Jsonthis! enables you to define custom serializers to transform property values during serialization. These can be set at the global, class, or field level, with priority given to field-level serializers over class-level serializers, and class-level serializers over global-level serializers.

Global Serializer

Register a global serializer for a specific type using Jsonthis.registerGlobalSerializer():

function dateSerializer(value: Date): string {
    return value.toUTCString();
}

class User {
    id: number = 1;
    registeredAt: Date = new Date();

    declare toJSON: () => any;
}

const jsonthis = new Jsonthis({models: [User]});
jsonthis.registerGlobalSerializer(Date, dateSerializer);

const user = new User();
console.log(user.toJSON());
// { id: 1, registeredAt: 'Sat, 27 Apr 2024 17:03:52 GMT' }

Class-Level Serializer

Define a custom serializer for a specific class using the @JsonSerializer decorator:

function valueSerializer(value: Value): string {
    return `${value.type}(${value.value})`;
}

@JsonSerializer(valueSerializer)
class Value {
    type: string;
    value: any;

    constructor(type: string, value: any) {
        this.type = type;
        this.value = value;
    }

    declare toJSON: () => any;
}

const jsonthis = new Jsonthis({models: [Value]});

const value = new Value("int", 123);
console.log(value.toJSON());
// "int(123)"

Field-Level Serializer

Utilize the @JsonField decorator to specify a custom serializer for a specific property:

function maskEmail(value: string): string {
    return value.replace(/(?<=.).(?=[^@]*?.@)/g, "*");
}

class User {
    id: number = 1;
    @JsonField({serializer: maskEmail})
    email: string = "[email protected]";

    declare toJSON: () => any;
}

const jsonthis = new Jsonthis({models: [User]});

const user = new User();
console.log(user.toJSON());
// { id: 1, email: 'j******[email protected]' }

Contextual Custom Serializer

Jsonthis! serialization supports a user-defined context object that can be used to further influence the serialization process. All serializers (global, class-level, field-level) can access the context object through the options parameter.

Here's an example of a field-level contextual custom serializer:

function maskEmail(value: string, options?: ToJsonOptions): string {
    return value.replace(/(?<=.).(?=[^@]*?.@)/g, options?.context?.maskChar || "*");
}

class User {
    id: number = 1;
    @JsonField({serializer: maskEmail})
    email: string = "[email protected]";

    declare toJSON: () => any;
}

const jsonthis = new Jsonthis({models: [User]});

const user = new User();
console.log(jsonthis.toJson(user, {context: {maskChar: "-"}}));
// { id: 1, email: '[email protected]' }

Limit Serialization Depth

You can limit the depth of serialization by setting the maxDepth option at global level in JsonthisOptions at construction time:

class User {
    id: number;
    name: string;
    friend?: User;

    constructor(id: number, name: string) {
        this.id = id;
        this.name = name;
    }

    declare toJSON: () => any;
}

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, models: [User]});

console.log(user.toJSON());
// { id: 1, name: 'John', friend: { id: 2, name: 'Jane' } }

You can also set the maxDepth option at the method level in ToJsonOptions:

const jsonthis = new Jsonthis({models: [User]});

console.log(jsonthis.toJson(user, {maxDepth: 1}));
// { id: 1, name: 'John', friend: { id: 2, name: 'Jane' } }

BigInt Serialization

By default, Jsonthis! serializes BigInt values as strings if they exceed the maximum safe integer value to avoid precision loss:

class User {
    id: bigint = BigInt(Number.MAX_SAFE_INTEGER) + 1n;
    declare toJSON: () => any;
}

new Jsonthis({models: [User]});
const user = new User();
console.log(user.toJSON());
// { id: '9007199254740992' }

console.log(JSON.stringify(user));
// {"id":"9007199254740992"}

However, you can change this behavior by setting the transformBigInt option to false. This is useful if you plan to use custom JSON implementations that can handle BigInt values, for example the json-bigint library:

import JSONBigInt from "json-bigint";

new Jsonthis({models: [User], transformBigInt: false});
const user = new User();
console.log(user.toJSON());
// { id: 9007199254740992n }

console.log(JSONBigInt.stringify(user));
// {"id":9007199254740992}

Circular References

Jsonthis! can detect circular references out of the box. When serializing an object with circular references, the default behavior is to throw a CircularReferenceError. However, you can customize this behavior by providing a custom handler:

function serializeCircularReference(value: any): any {
    return {$ref: `$${value.constructor.name}(${value.id})`};
}

class User {
    id: number;
    name: string;
    friend?: User;

    constructor(id: number, name: string) {
        this.id = id;
        this.name = name;
    }

    declare toJSON: () => any;
}

const jsonthis = new Jsonthis({models: [User], circularReferenceSerializer: serializeCircularReference});

const user = new User(1, "John");
user.friend = new User(2, "Jane");
user.friend.friend = user;

console.log(user.toJSON());
// {
//   id: 1,
//   name: 'John',
//   friend: { id: 2, name: 'Jane', friend: { '$ref': '$User(1)' } }
// }

Sequelize support

Jsonthis! seamlessly integrates with the Sequelize ORM library. To utilize Jsonthis! with Sequelize, simply specify it in the library constructor:

const sequelize = new Sequelize({...});

const jsonthis = new Jsonthis({
    sequelize: sequelize
});

Now, Jsonthis! will seamlessly intercept the serialization process when using the toJSON() method with Sequelize models:

function maskEmail(value: string): string {
    return value.replace(/(?<=.).(?=[^@]*?.@)/g, "*");
}

export class User extends Model<InferAttributes<User>, InferCreationAttributes<User>> {
    @Attribute(DataTypes.INTEGER)
    @PrimaryKey
    declare id: number;

    @Attribute(DataTypes.STRING)
    @NotNull
    @JsonField({serializer: maskEmail})
    declare email: string;

    @Attribute(DataTypes.STRING)
    @NotNull
    @JsonField(false)
    declare password: string;
}

const jsonthis = new Jsonthis({case: "snake", sequelize});

const user = await User.create({
    id: 1,
    email: "[email protected]",
    password: "s3cret"
});

console.log(user.toJSON());  // or jsonthis.toJson(user)
// {
//   id: 1,
//   email: 'j******[email protected]',
//   updated_at: 2024-04-20T12:58:10.229Z,
//   created_at: 2024-04-20T12:58:10.229Z
// }

Let's Contribute Together!

I'm excited to have you contribute and share your ideas to make this library even better!

How You Can Help

  1. Fork the repository.
  2. Create a new branch for your changes.
  3. Make your improvements, test them, and commit your work.
  4. Push your branch to your fork.
  5. Send us a pull request!

Some Tips

  • Keep the coding style consistent.
  • Write clear and friendly commit messages.
  • Don't forget tests and documentation!
  • Be kind and respectful in your interactions.

Got Ideas?

Share your thoughts! I'd love to hear your suggestions. Just open an issue and let's chat!

Thanks a bunch for considering lending a hand! 🌟