fastify / fastify-swagger

Swagger documentation generator for Fastify
MIT License
915 stars 201 forks source link

oneOf in querystring failing with correct querystring provided #810

Closed nathan-rogers closed 1 month ago

nathan-rogers commented 1 month ago

Prerequisites

Fastify version

4.26.2

Plugin version

No response

Node.js version

20.11.1

Operating system

Linux

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

Ubuntu 22.04.4 LTS

Description

Schema

 schema: {
    description: 'retrieve by name or id',
    querystring: {
      oneOf: [
        { type:'object', properties:{name: {type:'string'}}},
        { type:'object', properties:{id  : {type:'string'}}},
      ]
    }
}

Request http://localhost:3000/forms/form?name=asdf

Response

{
  "statusCode": 400,
  "code": "FST_ERR_VALIDATION",
  "error": "Bad Request",
  "message": "querystring must match exactly one schema in oneOf"
}

Link to code that reproduces the bug

No response

Expected Behavior

I expect the provided request example to pass validation, since I've provided oneOf either name or id in the querystring. I don't know what else to say. If this isn't a bug, how do I do this correctly?

gurgunday commented 1 month ago

Seems like a bug, anyOf works but not oneOf

Basic Test:

const fastify = require("fastify")({ logger: true });
const assert = require("assert");

// Define the route with the schema
fastify.get("/forms/form", {
  schema: {
    query: {
      type: "object",
      additionalProperties: false,
      oneOf: [
        {
          properties: {
            name: { type: "string" },
          },
        },
        {
          properties: {
            id: { type: "string" },
          },
        },
      ],
    },
  },
  handler: async (request, reply) => {
    return { query: request.query };
  },
});

fastify.setErrorHandler((error, request, reply) => {
  console.error(error);
  console.error(request.query);
});

// Test function
const runTest = async () => {
  try {
    await fastify.ready();

    // Test with 'name' parameter
    const response1 = await fastify.inject({
      method: "GET",
      url: "/forms/form?name=asdf",
    });

    assert.strictEqual(
      response1.statusCode,
      200,
      "Expected status code 200 for name parameter",
    );

    console.log("Test passed: name parameter works as expected");

    // Test with 'id' parameter
    const response2 = await fastify.inject({
      method: "GET",
      url: "/forms/form?id=123",
    });

    assert.strictEqual(
      response2.statusCode,
      200,
      "Expected status code 200 for id parameter",
    );

    console.log("Test passed: id parameter works as expected");

    console.log("All tests passed successfully");
  } catch (error) {
    console.error("Test failed:", error);
  } finally {
    await fastify.close();
  }
};

// Run the test
runTest();
climba03003 commented 1 month ago

It is more like the issue from ajv. Note that the option here is not same with fastify default, but it should success.

import Ajv from 'ajv'

const ajv = new Ajv()

const schema = {
  oneOf: [
    { type: "object", properties: { name : { type: 'string' } } },
    { type: "object", properties: { id : { type: 'string' } } },
  ]
}

const schema2 = {
  type: 'object',
  properties: {
    id : { type: 'string' },
    name : { type: 'string' },
  },
  oneOf: [
    { required: ['id'] },
    { required: ['name'] },
  ]
}

const validate = ajv.compile(schema)

console.log(validate({ id: 'foo' })) // false
console.log(validate({ name: 'foo' })) // false

const validate2 = ajv.compile(schema2)

console.log(validate2({ id: 'foo' })) // true
console.log(validate2({ name: 'foo' })) // true
climba03003 commented 1 month ago

After digging the issue deeper, your schema inside oneOf doesn't excluding each another. The reason is that both schema ACCEPT extra properties which leads to failing the oneOf check.

You should add additionalProperties inside the sub-schema, and disable removeAdditional.

const schema = {
  oneOf: [
    { type: 'object', properties: { name: { type: 'string' } }, additionalProperties: false },
    { type: 'object', properties: { id: { type: 'string' } }, additionalProperties: false },
  ]
}

const fastify = Fastify({
  ajv: {
    customOptions: {
      removeAdditional: false,
    }
  }
})

fastify.get('/', {
  schema: {
    querystring: schema
  }
}, async function (request) {
  return request.query
})

Or use required properties as discriminator.

const schema = {
  type: 'object',
  properties: {
    name: { type: 'string' },
    id: { type: 'string' }
  },
  additionalProperties: false,
  oneOf: [
    { required: ['name'] },
    { required: ['id'] }
  ]
}

const fastify = Fastify({})

fastify.get('/', {
  schema: {
    querystring: schema
  }
}, async function (request) {
  return request.query
})