question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Exclude/Expose decorator for properties in class inheritance

See original GitHub issue

I have 2 projects sharing some entities, and I’m trying to find a way to keep the validation consistent through all my application, and if possible, reuse part of the code instead of copying/pasting it.

My example

Classes needed

// This class describes the database table
class UserDB {
  id: string;         // generated by DB when a new record is added
  email: string;      // NOT NULL
  age?: number;       
  firstName?: string;
  lastName?: string;
}

// This class describes all fields validations
class User {
  @IsUUID("4") @JSONSchema({ description: `User's ID` })
  id: string;

  @IsEmail() @JSONSchema({ description: `User's email` })
  email: string;

  @IsOption() @IsInt() @IsPositive() @Min(18) @JSONSchema({ description: `User's age` })
  age?: number;

  @IsOption() @MinLength(3) @JSONSchema({ description: `User's first name` })
  firstName?: string;

  @IsOption() @MinLength(5) @JSONSchema({ description: `User's last name` })
  lastName?: string;
}

Create User API schemas

class CreateUserRequest {
  @IsEmail() @JSONSchema({ description: `User's email` })
  email: string;

  @IsOption() @IsInt() @IsPositive() @Min(18) @JSONSchema({ description: `User's age` })
  age?: number;

  @IsOption() @MinLength(3) @JSONSchema({ description: `User's first name` })
  firstName?: string;

  @IsOption() @MinLength(5) @JSONSchema({ description: `User's last name` })
  lastName?: string;
}

class CreateUserResponse extends User { }

Change Email API schemas

class ChangeEmailRequest {
  @IsEmail() @JSONSchema({ description: `New email address` })
  email: string;
}

class ChangeEmailResponse extends User { }

As we can see, we keep copying/pasting all the validations and descriptions from class to class, so I’m trying to find a better way to reuse the code so that it is also easier to maintain.

Solution 1

I create a common class containing the “base” properties.

class UserCommon {
  @IsEmail() @JSONSchema({ description: `User's email` })
  email: string;

  @IsOption() @IsInt() @IsPositive() @Min(18) @JSONSchema({ description: `User's age` })
  age?: number;

  @IsOption() @MinLength(3) @JSONSchema({ description: `User's first name` })
  firstName?: string;

  @IsOption() @MinLength(5) @JSONSchema({ description: `User's last name` })
  lastName?: string;
}

class User extends UserCommon {
  @IsUUID("4") @JSONSchema({ description: `User's ID` })
  id: string;
}

And then try to reuse the “base” class whenever possible

class CreateUserRequest extends UserCommon {}

class CreateUserResponse extends User {}

class ChangeEmailRequest {
  @IsEmail() @JSONSchema({ description: `User's email` })
  email: string;
}

class ChangeEmailResponse extends User {}

Solution 2

Create a base class describing all the fields with their validations.

class User {
  @IsUUID("4") @JSONSchema({ description: `User's ID` })
  id: string;
  
  @IsEmail() @JSONSchema({ description: `User's email` })
  email: string;

  @IsOption() @IsInt() @IsPositive() @Min(18) @JSONSchema({ description: `User's age` })
  age?: number;

  @IsOption() @MinLength(3) @JSONSchema({ description: `User's first name` })
  firstName?: string;

  @IsOption() @MinLength(5) @JSONSchema({ description: `User's last name` })
  lastName?: string;
}

And then extend it excluding or exposing fields

class CreateUserRequest extends User {
  @Exclude()
  id: string;
}

class CreateUserResponse extends User {}

class ChangeEmailRequest {
  @Expose()
  email: string;
}

class ChangeEmailResponse extends User {}

Solution 1 can be already implemented, even tho it will be hard to isolate the “common” properties when the app starts becoming big. i.e. if I introduce an UpdateUser API, probably I want to keep the email out of it, so I have to remove the email from the UserCommon class.

Solution 2 would be really flexible but I guess it is not supported currently by this library, right? Any chance to get this implemented?

Do you have any feedback? or any smarter way to achieve this result?

Issue Analytics

  • State:open
  • Created 4 years ago
  • Reactions:1
  • Comments:7 (4 by maintainers)

github_iconTop GitHub Comments

1reaction
bagbytecommented, Dec 4, 2019

@epiphone few feedback:

  1. validation groups are not supported by class-validator-jsonschema so the swagger file will not be effected
  2. implementing Pick will require the declaration of each property
  3. you still need to decorate each property

I’ve come out with a new solution much lighter.

// inheritValidations.ts

import { getFromContainer, MetadataStorage } from 'class-validator';
import { ValidationMetadata } from 'class-validator/metadata/ValidationMetadata';

export type ClassConstructor<T> = new () => T;

function strEnum<T extends string>(o: T[]): { [P in T]: P } {
    return o.reduce((res, key) => {
        res[key] = key;
        return res;
    }, Object.create(null));
}

export function inheritValidations<T, P extends keyof T>(NewClass: Function,
                                                         BaseClass: ClassConstructor<T>,
                                                         properties: P[]) {
    const propertiesObject = strEnum(properties);

    getFromContainer(MetadataStorage).getTargetValidationMetadatas(BaseClass, null)
        .filter(md => properties.includes(md.propertyName as any))
        .forEach((md) => {
            const validationMetadata = { ...md };
            validationMetadata.target = NewClass;

            getFromContainer(MetadataStorage).addValidationMetadata(new ValidationMetadata(validationMetadata));
        });

    return NewClass as new (...args: any) => Pick<T, keyof typeof propertiesObject>;
}

Now we can define all the validations on the User class, and create a new class using the validation already defined, specifying the list of properties to inherit (with their validations settings).

// GetUserRequest.ts

export class GetUserRequest extends inheritValidations(class GetUserRequest {}, User, ['id']) {}
0reactions
epiphonecommented, Dec 6, 2019

Doesn’t it work without keyOfStringsOnly: true if we extend strEnums type to something like function strEnum<T extends string | symbol | number>(o: T[]): { [P in T]: P }?

Read more comments on GitHub >

github_iconTop Results From Across the Web

property decorator behavior with inheritance in python
Here in the Derived class , I am trying to access the property attribute function fun_name using the Derived class member function sample_fun...
Read more >
class-transformer - npm
Skipping specific properties ⬆. Sometimes you want to skip some properties during transformation. This can be done using @Exclude decorator:.
Read more >
Python: Decorators in OOP - Towards Data Science
A guide on classmethods, staticmethods and the property decorator ... or writing a custom new method, metaclasses, and Multiple Inheritance.
Read more >
Python Property Decorator - TutorialsTeacher
The property decorator allow us to define properties easily without ... Use @property decorator on any method in the class to use the...
Read more >
Python @property Decorator (With Examples) - Programiz
setter . The @name.setter decorator is a way to define a method as a "setter" for a class attribute. When this method ...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found