fast-json-stringify icon indicating copy to clipboard operation
fast-json-stringify copied to clipboard

Custom Format Serialization Fails with Discriminated Unions but Works with Direct Schema

Open 123NeNaD opened this issue 3 months ago • 0 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

5.2.2

Plugin version

6.0.1

Node.js version

22.14.0

Operating system

Windows

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

10

Description

Bug Description

Custom AJV formats configured in serializerOpts work correctly when used directly in response schemas, but fail when the same schema is used within a discriminated union (TypeBox Type.Union).

Configuration

The custom objectId format is configured in both AJV validation and serialization:

const server: FastifyInstance = fastify({
  ajv: {
    customOptions: {
      coerceTypes: 'array',
      removeAdditional: 'all',
      useDefaults: true,
      formats: {
        objectId: {
          type: 'string',
          validate: (value) => ObjectId.isValid(value),
        },
      },
      keywords: ['transform'],
    },
  },
  serializerOpts: {
    ajv: {
      formats: {
        objectId: {
          type: 'string',
          validate: (value) => ObjectId.isValid(value),
        },
      },
    },
  },
});

Schema Definition

import { Type, Static } from '@sinclair/typebox';
import { ObjectId } from 'mongodb';

const BaseShapeSchema = Type.Object({
  _id: Type.Unsafe<ObjectId>({
    type: 'string',
    format: 'objectId',
  }),
  name: Type.String(),
});

const CircleSchema = Type.Intersect([
  BaseShapeSchema,
  Type.Object({
    type: Type.Literal('circle'),
    radius: Type.Number(),
  }),
]);

const RectangleSchema = Type.Intersect([
  BaseShapeSchema,
  Type.Object({
    type: Type.Literal('rectangle'),
    width: Type.Number(),
    height: Type.Number(),
  }),
]);

// Discriminated union
const ShapeSchema = Type.Union([CircleSchema, RectangleSchema]);

Expected Behavior

Both route configurations should serialize successfully and return the ObjectId as a string.

Actual Behavior

Works: Direct schema usage

server.route({
  method: 'GET',
  url: '/test',
  schema: {
    response: {
      200: Type.Object({
        shape: RectangleSchema, // Direct schema reference
      }),
    },
  },
  handler: async (request, reply) => {
    const rectangle = {
      type: 'rectangle',
      name: 'My Rectangle',
      _id: new ObjectId('507f1f77bcf86cd799439015'),
      width: 20,
      height: 15,
    };
    return reply.status(200).send({ shape: rectangle });
  },
});

Response: ✅ Success

{
  "shape": {
    "name": "My Rectangle",
    "_id": "507f1f77bcf86cd799439015",
    "type": "rectangle",
    "width": 20,
    "height": 15
  }
}

Fails: Discriminated union usage

server.route({
  method: 'GET',
  url: '/test',
  schema: {
    response: {
      200: Type.Object({
        shape: ShapeSchema, // Union schema reference
      }),
    },
  },
  handler: async (request, reply) => {
    const rectangle = {
      _id: new ObjectId('507f1f77bcf86cd799439015'),
      name: 'My Rectangle',
      type: 'rectangle',
      width: 20,
      height: 15,
    };
    return reply.status(200).send({ shape: rectangle });
  },
});

Error: ❌ Failure

TypeError: The value of '#/properties/shape' does not match schema definition.

Analysis

The issue appears to be that when using discriminated unions (Type.Union), the custom format is not being applied correctly by the serializer. The same schema works when referenced directly but fails when wrapped in a union type.

Link to code that reproduces the bug

https://github.com/fastify/fast-json-stringify

Expected Behavior

Both route configurations should serialize successfully and return the ObjectId as a string.

123NeNaD avatar Aug 25 '25 22:08 123NeNaD