Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Schema generated with Tuple<Union> incompatible to fast-json-stringify #144

Open
2 tasks done
colin240215 opened this issue May 7, 2024 · 4 comments
Open
2 tasks done

Comments

@colin240215
Copy link

Prerequisites

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

Fastify version

4.26.2

Plugin version

4.0.0

Node.js version

20.11.1

Operating system

Linux

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

Ubuntu 22.04.4

Description

Tuple<Union> leads to serialization error. The code below creates a fastify server with 3 endpoints. Each responds with exactly what it receives. Only the schemas are slightly different:

  • /array: acceptes Array<Union>
  • /unionTuple: accepts Tuple<Union>
  • /numTuple: accepts Tuple<Number>

One can reproduce the error by sending requests to /unionTuple with body [1, 2]. Everything is fine when sending the same request to the other 2 endpoints.

import fastify from 'fastify';
import { type TypeBoxTypeProvider } from '@fastify/type-provider-typebox';
import { Type } from '@sinclair/typebox';

const app = fastify(
  { ajv: { customOptions: { coerceTypes: false } } },
).withTypeProvider<TypeBoxTypeProvider>();

const StringOrNum = Type.Union([Type.String(), Type.Number()]);

const UnionArray = Type.Array(StringOrNum);
const ArraySchema = {
  body: UnionArray,
  response: {
    200: UnionArray,
  },
};
app.post('/array', { schema: ArraySchema }, (req, res) => {
  const array = req.body;
  res.status(200).send(array);
});

const UnionTuple = Type.Tuple([StringOrNum, StringOrNum]);
const UnionTupleSchema = {
  body: UnionTuple,
  response: {
    200: UnionTuple,
  },
};
app.post('/unionTuple', { schema: UnionTupleSchema }, (req, res) => {
  const tuple = req.body;
  res.status(200).send(tuple);
});

const NumTuple = Type.Tuple([Type.Number(), Type.Number()]);
const NumTupleSchema = {
  body: NumTuple,
  response: {
    200: NumTuple,
  },
};
app.post('/numTuple', { schema: NumTupleSchema }, (req, res) => {
  const tuple = req.body;
  res.status(200).send(tuple);
});

app.setErrorHandler((error, req, res) => {
  if ('serialization' in error) {
    res.status(500).send({ 'serialization error': error.message });
  } else {
    res.send(error);
  }
});

app.listen({ port: 3000 });

Link to code that reproduces the bug

No response

Expected Behavior

Type<Union> is expected to serialize the data successfully.
The endpoint have identical schema for request and response. So if data format is invalid, some exception should be thrown upon request validation. But the error I encountered happens during serialization, so I suspect that it must have something to do with fast-json-stringify failing to serialize the response body.

@colin240215
Copy link
Author

After further investigation, it seems like the generated schema serializer has a bug at the 4th if statement in anonymous0(). Inside this statement is an if-else block. Apparently, it always executes the else block and throw Error('Item at 0 does not match schema definition.'). (because the corresponding if condition is undefined).

(function anonymous(validator, serializer) {
    const JSON_STR_BEGIN_OBJECT = "{";
    const JSON_STR_END_OBJECT = "}";
    const JSON_STR_BEGIN_ARRAY = "[";
    const JSON_STR_END_ARRAY = "]";
    const JSON_STR_COMMA = ",";
    const JSON_STR_COLONS = ":";
    const JSON_STR_QUOTE = '"';
    const JSON_STR_EMPTY_OBJECT = JSON_STR_BEGIN_OBJECT + JSON_STR_END_OBJECT;
    const JSON_STR_EMPTY_ARRAY = JSON_STR_BEGIN_ARRAY + JSON_STR_END_ARRAY;
    const JSON_STR_EMPTY_STRING = JSON_STR_QUOTE + JSON_STR_QUOTE;
    const JSON_STR_NULL = "null";

    function anonymous0(obj) {
        // #

        if (obj === null) return JSON_STR_EMPTY_ARRAY;
        if (!Array.isArray(obj)) {
            throw new TypeError(`The value of '#' does not match schema definition.`);
        }
        const arrayLength = obj.length;

        if (arrayLength > 2) {
            throw new Error(`Item at 2 does not match schema definition.`);
        }

        const arrayEnd = arrayLength - 1;
        let value;
        let json = "";
        value = obj[0];
        if (0 < arrayLength) {
            if (undefined) { // ====== ====== ====== problem here ====== ====== ======
                if (validator.validate("__fjs_root_3#/items/0/anyOf/0", value))
                    if (typeof value !== "string") {
                        if (value === null) {
                            json += JSON_STR_EMPTY_STRING;
                        } else if (value instanceof Date) {
                            json += JSON_STR_QUOTE + value.toISOString() + JSON_STR_QUOTE;
                        } else if (value instanceof RegExp) {
                            json += serializer.asString(value.source);
                        } else {
                            json += serializer.asString(value.toString());
                        }
                    } else {
                        json += serializer.asString(value);
                    }
                else if (validator.validate("__fjs_root_3#/items/0/anyOf/1", value))
                    json += serializer.asNumber(value);
                else
                    throw new TypeError(
                        `The value of '#/items/0' does not match schema definition.`
                    );

                if (0 < arrayEnd) {
                    json += JSON_STR_COMMA;
                }
            } else {
                throw new Error(`Item at 0 does not match schema definition.`);
            }
        }
        value = obj[1];
        if (1 < arrayLength) {
            if (undefined) {
                if (validator.validate("__fjs_root_3#/items/1/anyOf/0", value))
                    if (typeof value !== "string") {
                        if (value === null) {
                            json += JSON_STR_EMPTY_STRING;
                        } else if (value instanceof Date) {
                            json += JSON_STR_QUOTE + value.toISOString() + JSON_STR_QUOTE;
                        } else if (value instanceof RegExp) {
                            json += serializer.asString(value.source);
                        } else {
                            json += serializer.asString(value.toString());
                        }
                    } else {
                        json += serializer.asString(value);
                    }
                else if (validator.validate("__fjs_root_3#/items/1/anyOf/1", value))
                    json += serializer.asNumber(value);
                else
                    throw new TypeError(
                        `The value of '#/items/1' does not match schema definition.`
                    );

                if (1 < arrayEnd) {
                    json += JSON_STR_COMMA;
                }
            } else {
                throw new Error(`Item at 1 does not match schema definition.`);
            }
        }

        return JSON_STR_BEGIN_ARRAY + json + JSON_STR_END_ARRAY;
    }
    const main = anonymous0;
    return main;
});

The schema serializer above can be reproduced by either:

  1. setting break point at node_modules/fastify/lib/reply.js
function serialize (context, data, statusCode, contentType) {
  const fnSerialize = getSchemaSerializer(context, statusCode, contentType)
  if (fnSerialize) {
    return fnSerialize(data) // ===== step into this function ======
  }
  return JSON.stringify(data)
}
  1. generate the schema with Typebox and serialize with fast-json-stringify
const FastJson = require('fast-json-stringify');
const { Type } = require('@sinclair/typebox');
const { TypeCompiler } = require('@sinclair/typebox/compiler');

const StringOrNum = Type.Union([Type.Number(), Type.String()]);
const UnionTuple = Type.Tuple([StringOrNum, StringOrNum]);

const schema = TypeCompiler.Compile(UnionTuple ).schema;
console.log(JSON.stringify(schema));

const serializer = FastJson(schema);
console.log(serializer([1, 2])); // ===== step into this function ======

@mcollina
Copy link
Member

why does fast-json-stringify fails at generating the correct code? What's the input JSON Schema that is causing this?

@colin240215
Copy link
Author

Tuple<Union> generates the schema below

{
  type: 'array',
  items: [
    { anyOf: [{ type: 'number' }, { type: 'string' }] },
    { anyOf: [{ type: 'number' }, { type: 'string' }] },
  ],
  additionalItems: false,
  minItems: 2,
  maxItems: 2,
}

Array<Union> generates this which works fine

{
  type: 'array',
  items: {
    anyOf: [{ type: 'number' }, { type: 'string' }],
  },
}

@mcollina
Copy link
Member

A PR with a fix for this would be really be nice. I guess items with multiple anyOf in it doesn't work.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants