fastify-type-provider-typebox
fastify-type-provider-typebox copied to clipboard
Schema generated with `Tuple<Union>` incompatible to `fast-json-stringify`
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
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: acceptesArray<Union>/unionTuple: acceptsTuple<Union>/numTuple: acceptsTuple<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.
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:
- 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)
}
- generate the schema with
Typeboxand serialize withfast-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 ======
why does fast-json-stringify fails at generating the correct code? What's the input JSON Schema that is causing this?
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' }],
},
}
A PR with a fix for this would be really be nice. I guess items with multiple anyOf in it doesn't work.