🚀 Executive Summary

TL;DR: JavaScript’s native `number` type cannot safely represent 64-bit integers (int64) without precision loss, necessitating their representation as strings. This guide provides Zod solutions using `.refine()` or `.superRefine()` with `BigInt` to validate int64 strings for both format and range, while preserving the string type.

🎯 Key Takeaways

  • JavaScript’s `number` type is an IEEE 754 double-precision float, safely representing integers only up to `Number.MAX_SAFE_INTEGER` (2^53 – 1), which is insufficient for int64 values.
  • Zod’s `z.coerce.number().int()` will fail or lose precision for int64 values, making custom validation necessary.
  • `BigInt` is the appropriate JavaScript type for accurately handling and validating numbers that exceed `Number.MAX_SAFE_INTEGER`, including int64 ranges.
  • Zod’s `.refine()` and `.superRefine()` methods enable custom validation logic, allowing checks against `BigInt` boundaries while ensuring the validated type remains a `string`.
  • Encapsulating int64 string validation logic into a reusable helper function (e.g., `zInt64()`) promotes DRY principles, improves code readability, and centralizes maintenance.

Zod: how to check if string is valid int64 while preserving string type?

Learn to validate if a string represents a valid 64-bit integer (int64) using Zod without coercing the type to a number. This guide explores several practical solutions for handling large numeric string inputs in TypeScript and Node.js environments.

The Problem: Validating Large Integers in JavaScript

In many systems, especially those interacting with databases (e.g., PostgreSQL’s BIGINT) or gRPC services, 64-bit integers (int64) are common. When these numbers are serialized in JSON, they are often represented as strings to avoid precision loss, as JavaScript’s native number type is an IEEE 754 double-precision float and can only safely represent integers up to Number.MAX_SAFE_INTEGER (which is 2^53 – 1, far smaller than the 2^63 – 1 of a signed int64).

Symptoms

You’re building an API or service with Zod and encounter the following challenges:

  • An incoming string field like "9223372036854775807" needs validation to ensure it’s a valid int64.
  • Using z.coerce.number().int() fails or loses precision for numbers larger than Number.MAX_SAFE_INTEGER.
  • You need to keep the validated value as a string to pass it to a database driver or another service that can handle large number strings correctly.
  • The goal is a Zod schema that confirms the string’s content represents an int64 but whose inferred type remains string.

Let’s explore three effective solutions to this problem, ranging from simple to robust and reusable.

Solution 1: Basic Validation with Regular Expressions

The simplest approach is to validate that the string contains only digits, with an optional leading minus sign. This doesn’t check the 64-bit range but is often sufficient for ensuring the string is a valid integer representation.

We can achieve this using z.string() combined with .regex() for a quick format check or .refine() for a more explicit validation.

Implementation

Here, we use .refine() to provide a clearer error message. The regex /^-?\d+$/ matches a string that starts with an optional hyphen and is followed by one or more digits.

import { z } from 'zod';

const integerStringSchema = z.string().refine((val) => /^-?\d+$/.test(val), {
  message: "Input must be an integer string.",
});

// --- Usage ---

// ✅ Valid
console.log(integerStringSchema.safeParse("12345"));
// Output: { success: true, data: '12345' }

console.log(integerStringSchema.safeParse("-987"));
// Output: { success: true, data: '-987' }

console.log(integerStringSchema.safeParse("999999999999999999999999")); // Passes format check
// Output: { success: true, data: '999999999999999999999999' }

// ❌ Invalid
console.log(integerStringSchema.safeParse("123.45"));
// Output: { success: false, ... }

console.log(integerStringSchema.safeParse("not-a-number"));
// Output: { success: false, ... }

This method is fast and straightforward but critically incomplete: it does not validate that the number fits within the int64 range.

Solution 2: Robust Range Validation with `BigInt`

For true int64 validation, you must check if the number falls within the correct range: from -(2^63) to (2^63 – 1). The best way to handle numbers of this magnitude in JavaScript is with the built-in BigInt type.

We can use .refine() to attempt parsing the string as a BigInt and then compare it to the int64 min/max boundaries.

Implementation

We define the int64 boundaries as BigInt constants and then use them in our refinement logic.

import { z } from 'zod';

// Define 64-bit integer boundaries using BigInt literals
const INT64_MIN = -9223372036854775808n;
const INT64_MAX = 9223372036854775807n;

const int64StringSchema = z.string()
  .regex(/^-?\d+$/, "Input must be a valid integer string")
  .refine((val) => {
    try {
      const num = BigInt(val);
      return num >= INT64_MIN && num <= INT64_MAX;
    } catch (e) {
      // Should not happen due to the regex, but as a safeguard
      return false;
    }
  }, {
    message: "Input is out of the signed 64-bit integer range.",
  });

// --- Usage ---

// ✅ Valid (within range)
console.log(int64StringSchema.safeParse("9223372036854775807"));
// Output: { success: true, data: '9223372036854775807' }

console.log(int64StringSchema.safeParse("-9223372036854775808"));
// Output: { success: true, data: '-9223372036854775808' }

// ❌ Invalid (out of range)
console.log(int64StringSchema.safeParse("9223372036854775808")); // (max + 1)
// Output: { success: false, ... }

// ❌ Invalid (bad format)
console.log(int64StringSchema.safeParse("123-456"));
// Output: { success: false, ... }

This approach is highly accurate and correctly enforces both the format and the range constraints, while ensuring the final data type remains a string.

Solution 3: Creating a Reusable Helper Function

If you need int64 string validation in multiple schemas across your application, repeating the .refine() logic is not ideal. A better practice is to encapsulate this logic into a reusable helper function that returns a configured Zod schema. This approach promotes DRY (Don’t Repeat Yourself) principles and leads to cleaner, more maintainable code.

Implementation

We’ll create a function, zInt64(), that builds and returns our complete validation schema. We’ll use .superRefine() here, which is ideal for complex validations that might add multiple issues.

import { z, ZodIssueCode } from 'zod';

const INT64_MIN_STR = "-9223372036854775808";
const INT64_MAX_STR = "9223372036854775807";
const INT64_MIN_BIGINT = BigInt(INT64_MIN_STR);
const INT64_MAX_BIGINT = BigInt(INT64_MAX_STR);

export function zInt64() {
  return z.string().superRefine((val, ctx) => {
    if (!/^-?\d+$/.test(val)) {
      ctx.addIssue({
        code: ZodIssueCode.custom,
        message: "Input must be a valid integer string.",
      });
      return;
    }

    try {
      const num = BigInt(val);
      if (num < INT64_MIN_BIGINT || num > INT64_MAX_BIGINT) {
        ctx.addIssue({
          code: ZodIssueCode.custom,
          message: `Input must be between ${INT64_MIN_STR} and ${INT64_MAX_STR}.`,
        });
      }
    } catch (e) {
      ctx.addIssue({
        code: ZodIssueCode.custom,
        message: "Failed to parse string as BigInt.",
      });
    }
  });
}

// --- Usage ---

// Now you can easily create schemas with this validation
const apiRequestSchema = z.object({
  userId: zInt64(),
  transactionId: zInt64(),
  someOtherField: z.string().min(1),
});

// ✅ Valid
console.log(apiRequestSchema.safeParse({
    userId: "123456789012345678",
    transactionId: "-1",
    someOtherField: "data"
}));
// Output: { success: true, data: { ... } }

// ❌ Invalid
console.log(apiRequestSchema.safeParse({
    userId: "999999999999999999999999999", // Out of range
    transactionId: "-1",
    someOtherField: "data"
}));
// Output: { success: false, ... }

This solution provides the same robust validation as Solution 2 but packages it in a clean, reusable, and self-documenting function.

Comparison of Solutions

Method Pros Cons Best For
1. Basic Regex
  • Very simple to implement.
  • Fastest performance.
  • Does not check numeric range.
  • Can allow invalid int64 values.
Scenarios where you only need to confirm the string looks like an integer, and range is not a concern or is validated elsewhere.
2. `refine` with `BigInt`
  • Completely accurate range and format validation.
  • Self-contained within a single schema definition.
  • Slightly more complex code.
  • Can be repetitive if used in many places.
One-off validations where correctness is critical and reusability is not a primary concern.
3. Reusable Helper
  • Promotes DRY principles.
  • Makes schemas clean and readable.
  • Centralizes complex logic for easy maintenance.
  • Requires setting up a separate helper function/file.
Any project where int64 string validation is needed in more than one place. This is the recommended approach for production applications.

Conclusion

When handling 64-bit integers as strings in a Node.js/TypeScript environment, Zod provides the flexibility to enforce strict validation rules while preserving the original string type. While a simple regex can check the format, using BigInt within a .refine() or .superRefine() block is the correct and safest way to validate the full int64 range. For maintainable and scalable applications, encapsulating this logic in a reusable helper function (Solution 3) is the most professional and robust approach.

Darian Vance - Lead Cloud Architect

Darian Vance

Lead Cloud Architect & DevOps Strategist

With over 12 years in system architecture and automation, Darian specializes in simplifying complex cloud infrastructures. An advocate for open-source solutions, he founded TechResolve to provide engineers with actionable, battle-tested troubleshooting guides and robust software alternatives.


🤖 Frequently Asked Questions

❓ Why can’t I simply use `z.coerce.number().int()` for int64 validation in Zod?

JavaScript’s `number` type is a double-precision float, which can only safely represent integers up to `Number.MAX_SAFE_INTEGER` (2^53 – 1). Int64 values (up to 2^63 – 1) exceed this limit, causing precision loss if coerced to a `number`.

❓ How do the different Zod int64 string validation solutions compare?

Basic regex is simple and fast but does not check the numeric range. Using `.refine()` with `BigInt` provides accurate format and range validation for one-off use. A reusable helper function with `.superRefine()` and `BigInt` is the most robust and maintainable approach for applications requiring int64 validation in multiple schemas.

❓ What is a common implementation pitfall when validating int64 strings with Zod?

A common pitfall is relying solely on a regular expression (e.g., `/-?\d+/`) to validate the string format without also performing a numeric range check using `BigInt`. This can lead to accepting strings that are syntactically integers but numerically fall outside the valid signed 64-bit integer range.

Leave a Reply

Discover more from TechResolve - SaaS Troubleshooting & Software Alternatives

Subscribe now to keep reading and get access to the full archive.

Continue reading