From 81519f7535cd1f578303776ef9b239785f33ab63 Mon Sep 17 00:00:00 2001 From: ifeanyi-ugwu Date: Wed, 8 Jul 2026 15:25:00 +0300 Subject: [PATCH 1/2] fix: correct jsonSchema definitions that reject valid values MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three scalars shipped a jsonSchema that rejects values the scalar itself accepts: - SESSN: the "patterns" were the format placeholders 'YYYYMMDDXXXX' / 'YYMMDDXXXX', used verbatim as regex patterns, so they matched only that literal text and no real personnummer. The `length` keyword beside them is not a JSON Schema keyword either. - PostalCode: oneOf over per-country patterns, but postal formats overlap — a 5-digit code matches US, DE, FR, IT, ES, ... at once — so every such code matches several patterns and fails oneOf's exactly-one requirement. - LocalDateTime: format 'date-time' requires an RFC 3339 timezone offset, which an offset-less LocalDateTime never has. --- src/scalars/LocalDateTime.ts | 5 ++++- src/scalars/PostalCode.ts | 7 +++++-- src/scalars/ssn/SE.ts | 16 +++++++++++----- 3 files changed, 20 insertions(+), 8 deletions(-) diff --git a/src/scalars/LocalDateTime.ts b/src/scalars/LocalDateTime.ts index 5dde9f6dd..7407e2fba 100644 --- a/src/scalars/LocalDateTime.ts +++ b/src/scalars/LocalDateTime.ts @@ -68,7 +68,10 @@ export const LocalDateTimeConfig: GraphQLScalarTypeConfig = /*#_ jsonSchema: { title: 'LocalDateTime', type: 'string', - format: 'date-time', + // Not format: 'date-time' — RFC 3339 date-time mandates a timezone offset, + // but a LocalDateTime is intentionally offset-less. Match what the scalar + // actually validates, like LocalDate/LocalTime do. + pattern: LOCAL_DATE_TIME_REGEX.source, }, }, }; diff --git a/src/scalars/PostalCode.ts b/src/scalars/PostalCode.ts index 0176cbaaa..438674887 100644 --- a/src/scalars/PostalCode.ts +++ b/src/scalars/PostalCode.ts @@ -121,8 +121,11 @@ export const GraphQLPostalCode = /*#__PURE__*/ new GraphQLScalarType({ codegenScalarType: 'string', jsonSchema: { title: 'PostalCode', - oneOf: POSTAL_CODE_REGEXES.map(regex => ({ - type: 'string', + type: 'string', + // anyOf, not oneOf: postal-code formats overlap across countries (e.g. a + // 5-digit code matches US, DE, FR, IT, ES, ... at once), so a valid code + // matches several patterns — oneOf would require it to match exactly one. + anyOf: POSTAL_CODE_REGEXES.map(regex => ({ pattern: regex.source, })), }, diff --git a/src/scalars/ssn/SE.ts b/src/scalars/ssn/SE.ts index 3fbf6cd1a..0724984a7 100644 --- a/src/scalars/ssn/SE.ts +++ b/src/scalars/ssn/SE.ts @@ -6,7 +6,14 @@ import { createGraphQLError } from '../../error.js'; // Algorithm: // https://swedish.identityinfo.net/personalidentitynumber -const SESSN_PATTERNS = ['YYYYMMDDXXXX', 'YYMMDDXXXX']; +// Structural patterns for the two accepted personnummer forms. Full validation +// (date + Luhn checksum) lives in _isValidSwedishPersonalNumber; these only +// describe the string shape for the jsonSchema. An optional -/+ separator is +// allowed since non-digits are stripped before validation. +const SESSN_PATTERNS = [ + /^\d{8}[-+]?\d{4}$/, // YYYYMMDD(-)XXXX — 12 digits + /^\d{6}[-+]?\d{4}$/, // YYMMDD(-)XXXX — 10 digits +]; function _isValidSwedishPersonalNumber(value: string): boolean { // Remove any non-digit characters @@ -128,10 +135,9 @@ export const GraphQLSESSN: GraphQLScalarType = /*#__PURE__*/ new GraphQLScalarTy codegenScalarType: 'string', jsonSchema: { title: 'SESSN', - oneOf: SESSN_PATTERNS.map((pattern: string) => ({ - type: 'string', - length: pattern.length, - pattern, + type: 'string', + anyOf: SESSN_PATTERNS.map(pattern => ({ + pattern: pattern.source, })), }, }, From 5e7ce49e8ed794a72e5c4d875e90a13bcbe2054c Mon Sep 17 00:00:00 2001 From: ifeanyi-ugwu Date: Wed, 8 Jul 2026 15:59:01 +0300 Subject: [PATCH 2/2] fix: align Timestamp/BigInt/USCurrency schemas with serialized output These jsonSchema definitions did not describe the value their scalar actually serializes: - Timestamp: declared type 'string', but serialize() returns a number of milliseconds (Date#getTime). 'unix-time' is also a non-standard, unenforceable format. - BigInt / Long: 'int64' implies a 64-bit bound a BigInt does not have, and type 'integer' alone omits the decimal-string form used for values beyond the safe-integer range. - USCurrency: the pattern forbade thousands separators and a leading minus, but serialize() emits locale-formatted strings like -$1,000.00, so the schema rejected the scalar's own output above $999 and for all negatives. --- src/scalars/BigInt.ts | 6 ++++-- src/scalars/Timestamp.ts | 4 ++-- src/scalars/USCurrency.ts | 4 +++- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/scalars/BigInt.ts b/src/scalars/BigInt.ts index aeddf68bc..57fea7e62 100644 --- a/src/scalars/BigInt.ts +++ b/src/scalars/BigInt.ts @@ -103,8 +103,10 @@ export const GraphQLBigIntConfig: GraphQLScalarTypeConfig< extensions: { codegenScalarType: 'bigint', jsonSchema: { - type: 'integer', - format: 'int64', + // Not format: 'int64' — a BigInt is arbitrary-precision, not 64-bit-bounded, + // and it serializes as a number when within the safe-integer range or as a + // decimal string when larger (to avoid precision loss). + oneOf: [{ type: 'integer' }, { type: 'string', pattern: '^-?\\d+$' }], }, }, }; diff --git a/src/scalars/Timestamp.ts b/src/scalars/Timestamp.ts index a3fa652a9..4e2997cc7 100644 --- a/src/scalars/Timestamp.ts +++ b/src/scalars/Timestamp.ts @@ -46,8 +46,8 @@ export const GraphQLTimestamp = /*#__PURE__*/ new GraphQLScalarType({ extensions: { codegenScalarType: 'Date | string | number', jsonSchema: { - type: 'string', - format: 'unix-time', + // serialize() returns a number of milliseconds (Date#getTime), not a string. + type: 'integer', }, }, }); diff --git a/src/scalars/USCurrency.ts b/src/scalars/USCurrency.ts index 014ec767d..ef0407038 100644 --- a/src/scalars/USCurrency.ts +++ b/src/scalars/USCurrency.ts @@ -64,7 +64,9 @@ export const GraphQLUSCurrency = /*#__PURE__*/ new GraphQLScalarType({ jsonSchema: { title: 'USCurrency', type: 'string', - pattern: '^\\$[0-9]+(\\.[0-9]{2})?$', + // Matches the serialized toLocaleString form: optional leading minus, '$', + // grouped thousands and always two decimals — e.g. $21.25 or -$1,000.00. + pattern: '^-?\\$[0-9]{1,3}(,[0-9]{3})*\\.[0-9]{2}$', }, }, });