routing-controllers icon indicating copy to clipboard operation
routing-controllers copied to clipboard

Validate array in Body decorator

Open hgranlund opened this issue 3 years ago • 7 comments

Description

Body of type array is not validated.

Minimal code-snippet showcasing the problem

 @Post('/save')
    save( @Body({ type: Hall, validate: true }) halls: Hall[]) { }

Expected behavior

The body should have been validated

Actual behavior

The body is not validated

hgranlund avatar Nov 24 '20 15:11 hgranlund

The same issue!

andy90rus avatar Feb 12 '21 12:02 andy90rus

I am having a similar issue. However, I am trying to setup @body to accept a json object from the request, but reject if an array is passed. So far it will run validation on the request body if a json object is passed, but not run validation if an array is passed.

example:

  public async exampleFunction(
    @Body({ required: true  }) requestBody: RequestValidator
  )

@NoNameProvided I believe this is similar to Issue 371

manofteal avatar Feb 19 '21 00:02 manofteal

Any updates on this?

7affer avatar May 27 '21 11:05 7affer

any update ?

abheypathogenie avatar Dec 29 '21 07:12 abheypathogenie

This is I think a typescript limitation at the moment. Reflector is not capable of returning the subtypes, it only emits that your type is an array, which is not enough for class-transformer to instantiate your desired target class. There could a solution to add the type parameter yes, but that will not solve primitive values. Since we cannot define types in the type option, you won't be able to indicate your subtype for primitve arrays.

This is a bit tricky question since class-validator won't run for primitive types without an encapsulating class anyway.

I think we have two ways to tackle this:

  1. Edit the BodyOptions to better align with ParamMetadataArgs and change transform to classTransform as other places and use transform as ParamMetadataArgs's transform to provide an interface on actually changing the value. With this approach we also have to change the order of execution because at the moment bodyParser runs after the transformation which results in an InvalidJsonError. This involves breaking changes.
  2. We have to provide additional properties to somehow cater to these scenarios. This would probably involve a lot of explicit checks for very specific things which would scale poorly in the future.

Personally I would go with scenario 1.

@NoNameProvided Any thoughts on this?

attilaorosz avatar Feb 20 '22 19:02 attilaorosz

Running into the same issue as @manofteal mentions. When passing an array (which I don't want), it gets passed to my controller method, unvalidated. Any update on this?

Edit: @attilaorosz I checked your two potential solutions, but shouldn't we just disable passing arrays as a body? Nest does it that way, because like you mentioned, it's a TS issue.

  1. https://github.com/nestjs/nest/issues/2874
  2. https://github.com/nestjs/nest/issues/335

driescroons avatar Aug 12 '22 13:08 driescroons

I had the same issue and was able to fix it by using this hack.

  • I intercepted the request body and changed the array to a nested array which can be accessed using a .data property. So for the controller this is what the body would look like.
 {
   data: [{ ... }, { ... }]
 }
  • I created a higher order class that takes a class and puts it as the type of its data property. We need this as routing-controllers cannot use Generics to deduce the correct type.
  • I reuse the HOC wherever I encounter an array body so the array elements are validated as per decorators inside them.

Let's see some code.

  • Request interceptor.
import bodyParser from 'body-parser';
import { NextFunction, Request, RequestHandler, Response } from 'express';
import {
  BadRequestError,
  ExpressMiddlewareInterface,
  Middleware
} from 'routing-controllers';
import { Service } from 'typedi';

@Middleware({ type: 'before' })
@Service()
class TransformArrayBody implements ExpressMiddlewareInterface {
  use(request: Request, response: Response, next: NextFunction): void {
    const json: RequestHandler = bodyParser.json();
    json(request, response, () => {
      if (!request.body || !(request.body instanceof Array)) {
        return next(
          new BadRequestError(
            'Body is invalid. Please send a valid array body.'
          )
        );
      }
      request.body = {
        data: request.body
      };
      if (next instanceof Function) {
        return next();
      }
    });
  }
}

export default TransformArrayBody;

  • The HOC and the class blueprint it returns (That's RequestDto<T>).
import { Type } from 'class-transformer';
import { ValidateNested } from 'class-validator';

// Class to annotate our request body.
class RequestDto<T> {
  data: T;
}

// HOC to create a dynamic class based on a class that is passed to it. This class has a "data" property which matches structure of one array item.
function CreateRequestDto<Impl, Arg>(
  Class: new (arg?: Arg) => Impl
): new (arg?: Arg) => RequestDto<Impl> {
  class Dto {
    @Type(() => Class)
    @ValidateNested()
    data: Impl;
  }
  return Dto;
}

export { CreateRequestDto, RequestDto };
  • The eventual usage. Voila!
@UseBefore(TransformArrayBody)
  async put(
    @Body({
      type: CreateRequestDto(MyClass),
      required: true
    })
    body: RequestDto<MyClass[]>,
    @Req() req: Request
  ): Promise<void> {
    console.log(body.data); // data has the original array that the client sent which completely typed and validated! :)
  }

MustansirZia avatar Mar 03 '23 13:03 MustansirZia