Skip to content

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

@123NeNaD

Description

@123NeNaD

Prerequisites

  • I have written a descriptive issue title
  • 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions