class-validator icon indicating copy to clipboard operation
class-validator copied to clipboard

question: Cannot read properties of undefined (reading 'constructor')

Open pieczorx opened this issue 3 years ago • 2 comments
trafficstars

I was trying to... Use class validator with my project.

The problem: App is starting but as soon as my class instance is created an error throws an app with exit code 1 and the following error:

TypeError: Cannot read properties of undefined (reading 'constructor') at ValidationExecutor.execute (PROJECT\node_modules\src\validation\ValidationExecutor.ts:57:14) at Validator.coreValidate (PROJECT\node_modules\src\validation\Validator.ts:107:14) at Validator.validate (PROJECT\node_modules\src\validation\Validator.ts:32:17) at validate (PROJECT\node_modules\src\index.ts:58:40) at AuthController.descriptor.value (webpack://PROJECT/./src/helpers/controllerValidationFactory.ts?:12:581)

index.ts

import {AuthController} from './AuthController'
const authController = new AuthController()

AuthController.ts

export class AuthController {
	@ValidateBody(LoginRequestBody)
	public async login(req: Request, res: Response): Promise<Response> {
		res.send('')
	}
}

controllerValidationFactory.ts

import {validate} from 'class-validator'
import {classToPlain, plainToClass} from 'class-transformer'

function validationFactory<T>(metadataKey: symbol, model: { new (...args: unknown[]): T}, source: 'body' | 'query' | 'params') {
  // eslint-disable-next-line @typescript-eslint/ban-types
  return function (target: unknown, propertyName: string, descriptor: TypedPropertyDescriptor<Function>) {
    Reflect.defineMetadata(metadataKey, model, target, propertyName)

    const method = descriptor.value
    descriptor.value = async function (...args) {
      const model = Reflect.getOwnMetadata(metadataKey, target, propertyName)

      const req = args[0]
      const next = args[2]
      const plain = req[source]
      const transformedClass = plainToClass(model,  plain)
      req[source] = classToPlain(transformedClass)
      const errors = await validate(transformedClass)
      if (errors.length > 0) {
        return next(errors)
      }
      return method.apply(this, args)
    }
  }
}

export const ValidateQuery = dto => validationFactory(Symbol('validate-query'), dto, 'query')
export const ValidateBody = dto  => validationFactory(Symbol('validate-body'), dto, 'body')
export const ValidateParams = dto  => validationFactory(Symbol('validate-params'), dto, 'params')

LoginRequestBody.ts

import {IsNotEmpty, IsString} from 'class-validator'

export class LoginRequestBody {
  @IsString()
  @IsNotEmpty()
    login: string

  @IsString()
  @IsNotEmpty()
    password: string
}

This is my project configuration

babel.config.js

module.exports = {
  presets: [
    '@babel/preset-typescript',
    ['@babel/preset-env', {
      targets: {
        node: '16.14',
      },
      loose: true,
    }],
  ],
  compact: true,
  plugins: [
    ['@babel/plugin-proposal-decorators', {
      version: 'legacy',
    }],
    ['@babel/plugin-proposal-class-properties', {
      loose: true,
    }],
    'babel-plugin-transform-typescript-metadata',
    ['babel-plugin-parameter-decorator', {
      legacy: true,
    }],
    '@babel/plugin-transform-runtime',
    '@babel/plugin-proposal-optional-chaining',
  ],
}

PS I'm using Inversify which works great. But as soon as I add class-validator things go wrong :/

Do you have any idea of what's going on?

pieczorx avatar Aug 18 '22 03:08 pieczorx

I ran into that error too just now. My assumption is that this line of code is being hit:

      const errors = await validate(transformedClass)

And, at that point transformedClass is undefined. I do not know what the intended behavior is for validate() if the object is undefined.

Here is what I observe using node repl:

$ node
Welcome to Node.js v16.13.0.
Type ".help" for more information.
> const classValidator = require('class-validator');
undefined
> classValidator.validate(undefined);
Uncaught TypeError: Cannot read properties of undefined (reading 'constructor')
    at ValidationExecutor.execute (/Users/jpurcell/src/bitbucket.org/five9psdev/ps-assets-npm-registry/node_modules/class-validator/cjs/validation/ValidationExecutor.js:46:90)
    at Validator.coreValidate (/Users/jpurcell/src/bitbucket.org/five9psdev/ps-assets-npm-registry/node_modules/class-validator/cjs/validation/Validator.js:49:18)
    at Validator.validate (/Users/jpurcell/src/bitbucket.org/five9psdev/ps-assets-npm-registry/node_modules/class-validator/cjs/validation/Validator.js:13:21)
    at Object.validate (/Users/jpurcell/src/bitbucket.org/five9psdev/ps-assets-npm-registry/node_modules/class-validator/cjs/index.js:40:73)

I tested a few values and found that these scenarios will cause the error when the argument is:

  • undefined
  • null
  • any string value, including empty string

But, the error does not happen on numbers, NaN, empty object, array, etc.

The NestJS ValidationPipe has this bit of interesting code: https://github.com/nestjs/nest/blob/master/packages/common/pipes/validation.pipe.ts#L168-L169. To me, this suggests that you should only execute class-validator's validate() function when the object being passed is a non-primitive type and is neither undefined nor null. Conceptually, this makes sense to me given that the mechanics of class-validator are to validate an object using the decorators defined on properties of the class, as opposed to validating primitives. For example, if you execute validate(1) how is class-validator supposed to know if that is valid? There's no decorator on primitive numbers to say what is valid or not.

My conclusion is that validating for undefined and other primitive type scenarios needs to happen prior to calling validate(). However, I did a brief look through documentation and didn't see anything to support this conclusion.

josephdpurcell avatar Aug 30 '22 13:08 josephdpurcell

You are definitely onto something, @josephdpurcell. This is a class validator, after all.

In my projects using class-validator I have created a handy helper class that uses class-transformer to convert plain objects to class instances and then runs validation on the instance. I extend that helper class in all my classes that use validation, and use a static method for validating the object.

I don't use ValidationPipes, so this is obviously quite a different scenario, but the code inside my check function is at least demonstration of a working example of using class-transformer to construct class instances from plain objects before validating. Hopefully there is something helpful in here?

Here's my validation helper class:

import { plainToInstance } from 'class-transformer';
import { ValidationError, validate, validateOrReject } from 'class-validator';

export type StaticThis<T> = { new (): T };

export abstract class Validation {
  static async check<T extends Validation>(
    this: StaticThis<T>,
    data: T,
    validate = true,
  ): Promise<T> {
    const that = plainToInstance(this, data);
    if (validate) {
      await validateOrReject(that);
    }

    return that;
  }
}

And here is how it is used:

import * as v from 'class-validator';
import { CreateUserInputViewModel } from '../presentation/view-models/user/user-inputs.view-model.js';
import { Validation } from '../shared/validation.js';

export class User extends Validation {
  @v.IsString()
  name: string;

  @v.IsEmail()
  @v.IsOptional()
  email?: string;

  static async fromCreateInput(
    data: CreateUserInputViewModel, // this is an input object with the same name and email fields
  ): Promise<User> {
    return this.check(data);
  }
}

braaar avatar Sep 02 '22 09:09 braaar

I don't know what is the root cause in the original message and I am not providing support for using different libraries.

However, the findings by @josephdpurcell are valid, class-validator should raise an error when a primitive value is passed.

Please open a related fix request and then we can close this issue.

NoNameProvided avatar Nov 13 '22 14:11 NoNameProvided

This issue has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.

github-actions[bot] avatar Dec 17 '22 00:12 github-actions[bot]