-
-
Notifications
You must be signed in to change notification settings - Fork 285
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[BUG] Model deserialization fails if class has an initializing constructor #1942
Comments
Hello @mfuqua3 Changing that will cause a potential breaking change. See you |
Also I’m not sure to understand why it broke the deserializer, excepted about the potential class properties pollution (introduced by your Object.assign()). All properties with annotation will be deserialized correctly. It doesn’t throw error. |
Hey @Romakita , thank you for the response.
Edit: I misread your comment and I understand the purpose. Here is additional context about my architecture and how I encountered this problem. I may need to determine an alternate way to what I'm going for. Due to the addition of This has also allowed me to encapsulate deserialization/serialization within our presentation layer, by limiting model interactions to within our controllers. Consider this example, which closely reflects the patterns of my system architecture: Data ContractsInterfaces defining the request parameters and expected response from my business layer export interface CreateUserRequest {
name: string;
email: string;
}
export interface User {
id: number;
name: string;
email: string;
} Business Layer InterfaceService component abstraction to be provided by the Ts.ED injector: export interface IUserService {
createUser(request: CreateUserRequest): Promise<User>;
}
export const IUserService: unique symbol = Symbol.for("IUserService"); TsED ModelImplements data contracts, defines JSON serialization behavior, validation, and schema for my OpenAPI spec. @Groups<UserModel>({
create: ["name", "email"],
info: ["id", "name", "email"],
})
export class UserModel implements CreateUserRequest, User {
constructor(init?: Partial<UserModel>) {
Object.assign(this, init);
}
@Required()
@Integer()
id: number;
@Required()
@Email()
@Property(String)
email: string;
@Required()
@Property(String)
name: string;
} ControllerTies all of the pieces together @Controller("/api/users")
export class UsersController {
constructor(@Inject(IUserService) private readonly userService: IUserService) {
}
@Post("/")
@Summary("Create a New User")
@Returns(201, UserModel).Groups("info")
async createUser(@BodyParams() @Groups("create") request: UserModel): Promise<UserModel> {
const createdUser = await this.userService.createUser(request);
return new UserModel(createdUser); //<-- Object init constructor is used to assign response to TsED model
}
} This is my current architecture and the constructor in the model (used by the final step of my controller) is my current issue. |
Hello @mfuqua3 Sorry, I thought I answered you on the last comment but it seems that the message got lost. I see your issue. But I cannot change the json-mapper behavior. But you can simplify your code. When you us @Controller("/api/users")
export class UsersController {
constructor(@Inject(IUserService) private readonly userService: IUserService) {
}
@Post("/")
@Summary("Create a New User")
@Returns(201, UserModel).Groups("info")
async createUser(@BodyParams() @Groups("create") request: UserModel): Promise<UserModel> {
return this.userService.createUser(request);
}
} This feature works with So it should solve partially your problem. To avoid prototype pollution you have to spread object and pick each property value before assignment: export class UserModel implements CreateUserRequest, User {
constructor({id, name, email}?: Partial<UserModel> = {}) {
Object.assign(this, {id, name, email});
}
} But this code is similar to: import {deserialize} from "@tsed/json-mapper";
const instance = deserialize<UserModel>(obj, {type: UserModel}); See you |
Thank you, this is enormously helpful feedback. I will adjust our application per your suggestions, thank you for your time and all of the work you do on this framework. We can consider this issue closed. |
I'll consider your issue for the next major version. Your constructor example give me a potential security issue ^^. |
Ok I'll introduce a new configuration option to disable this behavior in v6: @Constructor({
jsonMapper: {
disableUnsecureConstructor: true
}
}) v7 will set this option to true by default. |
PR created: #2059 |
🎉 This issue has been resolved in version 6.128.9 🎉 The release is available on:
Your semantic-release bot 📦🚀 |
🎉 This issue has been resolved in version 7.0.0-rc.4 🎉 The release is available on:
Your semantic-release bot 📦🚀 |
🎉 This issue has been resolved in version 7.0.0-rc.6 🎉 The release is available on:
Your semantic-release bot 📦🚀 |
🎉 This issue has been resolved in version 7.0.0 🎉 The release is available on:
Your semantic-release bot 📦🚀 |
Information
@tsed/json-mapper
The goal is to support the use of C#-esque "object initializer" syntax within domain TsED models. Currently this type of constructor will break the deserializer
Example
Steps to Reproduce:
Bug Test Controller
Test the endpoint
Result
JSON Schema:
Deserialized Model:
Acceptance criteria
Constructor example:
Additional Context
Apparent root cause of the issue is line 121 of
deserialize.ts
(plainObjectToClass function).const out: any = new type(src);
From what I can tell, passing
src
as a constructor parameter totype
is unnecessary to the operation of this function but creates the object in a pre-populated state in this specific use case. The deserializer does not expect the object to be pre-populated and does not sanitize the extra properties.The text was updated successfully, but these errors were encountered: