serializationjsonschemafastify

Fastify response serialization using anyOf is not working properly


I'm having trouble with response serialization.

My controller might return differing values based on the user's state. The schema that I wrote uses anyOf and definition of those schemas that the controller might return.

But for some reason 1 of them fails and empty object is returned. For others, correct response value is serialized and returned.

I have created reproduceable repo at this link. To reproduce run npm start and from a terminal (or browser) use:

curl http://localhost:3000

Above command returns correct data, however below doesn't

http://localhost:3000?id=1

Code from the link:

// Require the framework and instantiate it
const fastify = require("fastify")({ logger: false });

// Declare a route
fastify.route({
  method: "POST",
  schema: {
    response: {
      200: {
        type: "object",
        properties: {
          code: { type: "number", default: 1234 },
          data: {
            anyOf: [
              {
                type: "object",
                properties: {
                  token: { type: "string" },
                  user: {
                    type: "object",
                    properties: {
                      id: { type: "number" },
                      phoneNumber: { type: "string" },
                    },
                  },
                },
              },
              {
                type: "object",
                properties: {
                  tempToken: { type: "string" },
                  multipleChoice: {
                    type: "object",
                    properties: {
                      usernames: { type: "array", items: { type: "string" } },
                      locationAddresses: {
                        type: "array",
                        items: { type: "string" },
                      },
                    },
                  },
                },
              },
              {
                type: "object",
                properties: {
                  token: { type: "string" },
                  user: {
                    type: "object",
                    properties: {
                      id: { type: "number" },
                      username: { type: "string" },
                    },
                  },
                },
              },
            ],
          },
        },
      },
    },
  },
  url: "/",
  handler: async (request, reply) => {
    if (request.query.id === "1") {
      console.log("matching 1");
      return {
        code: 1000,
        data: {
          tempToken: "1234567890",
          multipleChoice: {
            usernames: ["user1", "user2"],
            locationAddresses: ["location1", "location2"],
          },
        },
      };
    }
    return {
      code: 1000,
      data: {
        token: "1234567890",
        user: { id: 1, username: "user1" },
      },
    };
  },
});

// Run the server!
fastify.listen({ port: 3000 }, (err) => {
  console.log("server started");
  if (err) {
    fastify.log.error(err);
    process.exit(1);
  }
});

Please, help me figure out what is causing the issue.


Solution

  • Fastify uses fast-json-stringify for the serialization of a response's body.

    From the docs on anyOf:

    The different schemas will be tested in the specified order. The more schemas stringify has to try before finding a match, the slower it will be.

    This description tells us that schemas will tried (for match) in the order they are listed in the anyOf. So, in a sense it will be tested in a manner similar to the way if ... else if ... else if .... works.

    For that reason, you need to make sure that only the right schema (other schemas should not match) is matched against the response returned by fastify server application.

    Hence, to fix the problem in question, you must keep these points in mind:

    1. Mention the schemas in the right order (keeping in mind that they are tested one by one from top to bottom).
    2. Make sure no other than the desired schema matches the response. You can take help of required for this purpose.

    Here is the fixed code (I have added required arrays for all properties):

    data: {
        anyOf: [
            {
            type: "object",
            properties: {
                token: { type: "string" },
                user: {
                type: "object",
                properties: {
                    id: { type: "number" },
                    phoneNumber: { type: "string" },
                },
                required: ["id", "phoneNumber"], // Use Required
                },
            },
            required: ["token", "user"], // Use Required
            },
            {
            type: "object",
            properties: {
                tempToken: { type: "string" },
                multipleChoice: {
                type: "object",
                properties: {
                    usernames: { type: "array", items: { type: "string" } },
                    locationAddresses: {
                    type: "array",
                    items: { type: "string" },
                    },
                },
                required: ["usernames", "locationAddresses"], // Use Required
                },
            },
            required: ["tempToken", "multipleChoice"], // Use Required
            },
            {
            type: "object",
            properties: {
                token: { type: "string" },
                user: {
                type: "object",
                properties: {
                    id: { type: "number" },
                    username: { type: "string" },
                },
                required: ["id", "username"], // Use Required
                },
            },
            required: ["token", "user"], // Use Required
            },
        ],
    },