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.
- Installation
- Getting Started
- Change Property Visibility
- Customizing Serialization
- Circular References
- Sequelize support
- Let's Contribute Together!
To install Jsonthis!, simply run:
npm install jsonthis
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.
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
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;
// ...
}
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]' } }
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 }
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.
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' }
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)"
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]' }
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]' }
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' } }
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}
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)' } }
// }
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
// }
I'm excited to have you contribute and share your ideas to make this library even better!
- Fork the repository.
- Create a new branch for your changes.
- Make your improvements, test them, and commit your work.
- Push your branch to your fork.
- Send us a pull request!
- Keep the coding style consistent.
- Write clear and friendly commit messages.
- Don't forget tests and documentation!
- Be kind and respectful in your interactions.
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! 🌟