fastify-multipart icon indicating copy to clipboard operation
fastify-multipart copied to clipboard

The coerceTypes: 'array' doesn't coerce one parameter to a single element in array

Open meotimdihia opened this issue 3 years ago • 9 comments

Prerequisites

  • [X] I have written a descriptive issue title
  • [X] I have searched existing issues to ensure the bug has not already been reported

Fastify version

3.x.x

Plugin version

5.x.x

Node.js version

14.x

Operating system

Windows

Operating system version (i.e. 20.04, 11.3, 10)

10

Description

I have got this error when send a POST request.

{statusCode: 400, error: "Bad Request", message: "body.authors should be array"}
error: "Bad Request"
message: "body.authors should be array"
statusCode: 400

My schema

  const body = {
    type: "object",
    properties: {
      authors: {
        type: "array",
        items: {
          type: "object"
        }
      }
   }

Fastify configuration

server.register(fastifyMultipart, {
 attachFieldsToBody: true,
 sharedSchemaId: "#mySharedSchema",
})
const ajv = new Ajv({
 removeAdditional: true,
 useDefaults: true,
 coerceTypes: "array",
 nullable: true
})
fastify.setValidatorCompiler(({ schema }) => {
 return ajv.compile(schema)
})

Steps to Reproduce

It is straightforward.

Expected Behavior

No response

meotimdihia avatar Jan 17 '22 16:01 meotimdihia

Can you please include a reproducible example? It might be straightforward but it will greatly simplify fixing it. You could also send a PR, it would be highly welcomed.

mcollina avatar Jan 17 '22 20:01 mcollina

Hi, I dunno how to create a reproducible example that is easy for everyone to see.

But I wrote a test, could you copy it to the test directory of fastify-multipart to check this problem: https://gist.github.com/meotimdihia/53299a9501843987907a369f445898ac

If I uncomment line 79 then the test will be passed.

meotimdihia avatar Jan 18 '22 07:01 meotimdihia

I've taken a look at this and I think without a reproducible example it's not going to be possible to definitively conclude on this issue. To me it looks as though it could be to do with the way you are registering routes and the validation on your fastify server, as in the below example where username gets coerced into an array.

import Fastify from 'fastify'
import Ajv from 'ajv'
import fastifyMultipart from 'fastify-multipart'

function buildServer() {
  const fastify = Fastify({
    logger: {
      prettyPrint: true,
    },
  })

  const ajv = new Ajv({
    removeAdditional: true,
    useDefaults: true,
    coerceTypes: 'array',
  })

  const opts = {
    attachFieldsToBody: true,
    sharedSchemaId: '#sharedLoginSchema',
  }
  fastify.register(fastifyMultipart, opts)

  fastify.setValidatorCompiler(({ schema }) => ajv.compile(schema))

// works
  fastify.register(async function (fs) {
    fs.post(
      '/login',
      {
        schema: {
          body: {
            type: 'object',
            properties: {
              username: {
                type: 'array',
              },
              password: {
                type: 'string',
              },
            },
          },
        },
      },
      async req => {
        const { username, password } = req.body
        return { username, password }
      }
    )
  })

// doesn't work
  fastify.post(
    '/login2',
    {
      schema: { body: fastify.getSchema('login') },
      schema: {
        body: {
          type: 'object',
          properties: {
            username: {
              type: 'array',
            },
            password: {
              type: 'string',
            },
          },
        },
      },
    },
    async req => {
      const { username, password } = req.body
      return { username, password }
    }
  )

  fastify.log.info('Fastify is starting up!')

  return fastify
}

const fastify = buildServer()

const start = async function () {
  try {
    await fastify.listen(3000)
  } catch (err) {
    fastify.log.error(err)
    process.exit(1)
  }
}

start()

If this is the cause of the confusion (and I agree it is not obvious that you might register the route like this) then perhaps the solution is simply to update the docs.

Also, from what I can tell, this isn't to do with fastify-multipart as the same behaviour is seen on all fastify route validation.

andrewwood2 avatar Mar 21 '22 16:03 andrewwood2

@andrewwood2 could you send a PR to update the docs?

mcollina avatar Mar 22 '22 09:03 mcollina

I am having a similar (or same problem), where I define a property of my schema as a { type: 'array', items: fastify.getSchema('mySharedSchema')}'. When I send only one file, it says that field expect an array. I am using "coerceTypes": Array, so I expected that it would work without any workaround. As a temporary fix I am doing a anyOf schema with a single File and a array FileArray.

rubenferreira97 avatar Apr 07 '22 09:04 rubenferreira97

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

stale[bot] avatar Apr 28 '22 03:04 stale[bot]

Hi, I added the example: https://github.com/meotimdihia/fastify-multipart-bug-report-2

you'll get this error:

Untitled 20

P/S: Please also re-open this issue: https://github.com/fastify/fastify-multipart/issues/313

meotimdihia avatar Sep 11 '22 10:09 meotimdihia

Also bumped into this issue today trying to get a 1 or more files field to work. It seems that ajv will only coerce scalar types as defined in https://github.com/ajv-validator/ajv/blob/master/lib/compile/validate/dataType.ts#L126

For multipart file uploads, dataType is object and thus not automatically coerced.

A potential fix would be to change the filesFromFields function to somehow detect if the underlying schema wants array types. Replacing the schema with oneOf hinders the swagger ui for file upload.

Titou325 avatar Mar 27 '24 23:03 Titou325

I worked around this by creating a custom ajv plugin

import type Ajv from "ajv";
import { DataValidateFunction } from "ajv/dist/types";

export const ajvArrayCoercion = (ajv: Ajv) => {
  ajv.addKeyword({
    keyword: "coerceObjectArray",
    modifying: true,
    compile: (schema, parent) => {
      delete parent.coerceObjectArray;

      const validator: DataValidateFunction = (data, dataCxt) => {
        if (!Array.isArray(data) && typeof data === "object") {
          // @ts-expect-error No typings for package ajv
          dataCxt.parentData[dataCxt.parentDataProperty] = [data];
        }

        return true;
      };

      return validator;
    },
    before: "array",
  });
  return ajv;
};

Which I then use as

multiFileField: {
  allOf: [
    {
      coerceObjectArray: true,
    },
    {
      type: "array",
      items: {
        isFile: true,
      },
    },
  ],
} as unknown as {
  type: "array";
  items: {
    isFile: true;
  };
}

Titou325 avatar Mar 28 '24 05:03 Titou325