From 2fd57a345e7c4877804fb3dc39c3cc641deb723b Mon Sep 17 00:00:00 2001 From: caballeto Date: Tue, 16 Jun 2026 16:56:46 +0200 Subject: [PATCH 1/2] fix: relax response-DTO schemas from .strict() to .passthrough() Response-shape DTOs now use .passthrough() so new fields added to the API don't break surfaces built against an older spec. Request schemas keep .strict() for typo detection in user input. Implements Postel's Law: strict on requests, tolerant on responses. Co-authored-by: Cursor --- scripts/generate-schemas.js | 3 +- scripts/lib/preprocess.mjs | 40 ++++ src/generated/schemas.ts | 382 ++++++++++++++++++------------------ 3 files changed, 233 insertions(+), 192 deletions(-) diff --git a/scripts/generate-schemas.js b/scripts/generate-schemas.js index b637763..3083cda 100644 --- a/scripts/generate-schemas.js +++ b/scripts/generate-schemas.js @@ -13,7 +13,7 @@ import { readFileSync, writeFileSync, mkdirSync, unlinkSync } from 'node:fs' import { dirname, join } from 'node:path' import { fileURLToPath } from 'node:url' -import { preprocessSpec, rewriteUnionsAsDiscriminated } from './lib/preprocess.mjs' +import { preprocessSpec, rewriteUnionsAsDiscriminated, relaxResponseStrict } from './lib/preprocess.mjs' import { generateZodClientFromOpenAPI } from 'openapi-zod-client' const __dirname = dirname(fileURLToPath(import.meta.url)) @@ -164,6 +164,7 @@ async function main() { const raw = readFileSync(tempGenerated, 'utf8') let clean = extractSchemas(raw) clean = rewriteUnionsAsDiscriminated(clean, inlinedDiscriminators) + clean = relaxResponseStrict(clean) clean = fixZod4RecordCalls(clean) mkdirSync(dirname(OUTPUT_PATH), { recursive: true }) diff --git a/scripts/lib/preprocess.mjs b/scripts/lib/preprocess.mjs index 6193148..ef6f41f 100644 --- a/scripts/lib/preprocess.mjs +++ b/scripts/lib/preprocess.mjs @@ -436,3 +436,43 @@ export function rewriteUnionsAsDiscriminated(source, unions) { return `z.discriminatedUnion("${disc}", [${memberList.join(', ')}])`; }); } + +/** + * Rewrite `.strict()` → `.passthrough()` on response-shape DTO declarations + * so surfaces tolerate new fields added to the API after the surface was built. + * Request schemas keep `.strict()` to catch typos in user input. + */ +export function relaxResponseStrict(source) { + const declRe = /^const\s+(\w+)\b/gm; + const strictRe = /\.strict\(\)/g; + + function isResponseShape(name) { + if (/^[a-z]/.test(name)) return false; + if (/(Request|Params)$/.test(name)) return false; + return ( + /(Dto|Response)$/.test(name) || + /^(SingleValueResponse|TableValueResult|CursorPage)/.test(name) + ); + } + + const declarations = []; + let match; + while ((match = declRe.exec(source)) !== null) { + declarations.push({ start: match.index, name: match[1] }); + } + if (declarations.length === 0) return source; + + let out = source.slice(0, declarations[0].start); + for (let i = 0; i < declarations.length; i++) { + const { start, name } = declarations[i]; + const end = + i + 1 < declarations.length ? declarations[i + 1].start : source.length; + const body = source.slice(start, end); + if (isResponseShape(name)) { + out += body.replace(strictRe, '.passthrough()'); + } else { + out += body; + } + } + return out; +} diff --git a/src/generated/schemas.ts b/src/generated/schemas.ts index fa7263d..c1c04ef 100644 --- a/src/generated/schemas.ts +++ b/src/generated/schemas.ts @@ -24,7 +24,7 @@ const ErrorResponse = z requestId: z.string().nullish(), errors: z.array(ErrorEntry.nullable()).nullish(), }) - .strict(); + .passthrough(); const DatadogChannelConfig = z .object({ channelType: z.literal("datadog"), @@ -1458,7 +1458,7 @@ const AlertChannelDto = z lastDeliveryAt: z.string().datetime({ offset: true }).nullish(), lastDeliveryStatus: z.string().nullish(), }) - .strict(); + .passthrough(); const AlertDeliveryDto = z .object({ id: z.string().uuid(), @@ -1478,7 +1478,7 @@ const AlertDeliveryDto = z errorMessage: z.string().nullish(), createdAt: z.string().datetime({ offset: true }), }) - .strict(); + .passthrough(); const ApiKeyCreateResponse = z .object({ id: z.number().int(), @@ -1487,7 +1487,7 @@ const ApiKeyCreateResponse = z createdAt: z.string().datetime({ offset: true }), expiresAt: z.string().datetime({ offset: true }).nullish(), }) - .strict(); + .passthrough(); const ApiKeyDto = z .object({ id: z.number().int(), @@ -1499,7 +1499,7 @@ const ApiKeyDto = z revokedAt: z.string().datetime({ offset: true }).nullish(), expiresAt: z.string().datetime({ offset: true }).nullish(), }) - .strict(); + .passthrough(); const AssertionResultDto = z .object({ type: z.string(), @@ -1509,7 +1509,7 @@ const AssertionResultDto = z expected: z.string().nullish(), actual: z.string().nullish(), }) - .strict(); + .passthrough(); const AssertionTestResultDto = z .object({ assertionType: z.string(), @@ -1519,7 +1519,7 @@ const AssertionTestResultDto = z expected: z.string().nullish(), actual: z.string().nullish(), }) - .strict(); + .passthrough(); const MemberRoleChangedMetadata = z .object({ kind: z.literal("member_role_changed"), @@ -1540,7 +1540,7 @@ const AuditEventDto = z metadata: AuditMetadata.nullish(), createdAt: z.string().datetime({ offset: true }), }) - .strict(); + .passthrough(); const KeyInfo = z .object({ id: z.number().int(), @@ -1558,7 +1558,7 @@ const EntitlementDto = z defaultValue: z.number().int(), overridden: z.boolean(), }) - .strict(); + .passthrough(); const PlanInfo = z .object({ tier: z.enum(["FREE", "STARTER", "PRO", "TEAM", "BUSINESS", "ENTERPRISE"]), @@ -1583,7 +1583,7 @@ const AuthMeResponse = z plan: PlanInfo, rateLimits: RateLimitInfo, }) - .strict(); + .passthrough(); const IncidentRef = z .object({ id: z.string().uuid(), title: z.string(), impact: z.string() }) .strict(); @@ -1596,10 +1596,10 @@ const ComponentUptimeDayDto = z uptimePercentage: z.number(), incidents: z.array(IncidentRef).nullish(), }) - .strict(); + .passthrough(); const BatchComponentUptimeDto = z .object({ components: z.record(z.string(), z.array(ComponentUptimeDayDto)) }) - .strict(); + .passthrough(); const FailureDetail = z .object({ monitorId: z.string().uuid(), reason: z.string() }) .strict(); @@ -1611,7 +1611,7 @@ const BulkMonitorActionResult = z .strict(); const CategoryDto = z .object({ category: z.string(), serviceCount: z.number().int() }) - .strict(); + .passthrough(); const ChartBucketDto = z .object({ bucket: z.string().datetime({ offset: true }), @@ -1620,7 +1620,7 @@ const ChartBucketDto = z p95LatencyMs: z.number().nullish(), p99LatencyMs: z.number().nullish(), }) - .strict(); + .passthrough(); const TlsInfoDto = z .object({ subjectCn: z.string().nullable(), @@ -1635,7 +1635,7 @@ const TlsInfoDto = z chainValid: z.boolean().nullable(), }) .partial() - .strict(); + .passthrough(); const TimingPhasesDto = z .object({ dns_ms: z.number().int().nullable(), @@ -1646,7 +1646,7 @@ const TimingPhasesDto = z total_ms: z.number().int().nullable(), }) .partial() - .strict(); + .passthrough(); const Http = z .object({ check_type: z.literal("http"), @@ -1729,7 +1729,7 @@ const CheckResultDetailsDto = z checkDetails: CheckTypeDetailsDto.nullable(), }) .partial() - .strict(); + .passthrough(); const CheckResultDto = z .object({ id: z.string().uuid(), @@ -1742,7 +1742,7 @@ const CheckResultDto = z details: CheckResultDetailsDto.nullish(), checkId: z.string().uuid().nullish(), }) - .strict(); + .passthrough(); const RuleEvaluationDto = z .object({ id: z.string().uuid(), @@ -1755,12 +1755,12 @@ const RuleEvaluationDto = z ruleScope: z.string().min(1), inputResultIds: z.array(z.string().uuid()).min(1), outputMatched: z.boolean(), - evaluationDetails: z.record(z.string(), z.object({}).partial().strict()), + evaluationDetails: z.record(z.string(), z.object({}).partial().passthrough()), engineVersion: z.string().min(1), checkId: z.string().uuid(), triggeringTransitionId: z.string().uuid().nullish(), }) - .strict(); + .passthrough(); const StateTransitionDetails = z .object({ source: z.enum(["pipeline", "public-api"]) }) .strict(); @@ -1780,16 +1780,16 @@ const IncidentStateTransitionDto = z checkId: z.string().uuid(), details: StateTransitionDetails, }) - .strict(); + .passthrough(); const PolicySnapshotDto = z .object({ hashHex: z.string().min(1), - policy: z.record(z.string(), z.object({}).partial().strict()), + policy: z.record(z.string(), z.object({}).partial().passthrough()), engineVersion: z.string().min(1), firstSeenAt: z.string().datetime({ offset: true }), lastSeenAt: z.string().datetime({ offset: true }), }) - .strict(); + .passthrough(); const CheckTraceDto = z .object({ checkId: z.string().uuid(), @@ -1797,7 +1797,7 @@ const CheckTraceDto = z transitions: z.array(IncidentStateTransitionDto), policySnapshot: PolicySnapshotDto.nullish(), }) - .strict(); + .passthrough(); const ComponentImpact = z .object({ componentId: z.string().uuid(), @@ -1814,10 +1814,10 @@ const ComponentsSummaryDto = z includedCount: z.number().int(), groupComponentCounts: z.record(z.string(), z.number().int()), }) - .strict(); + .passthrough(); const ComponentStatusDto = z .object({ id: z.string(), name: z.string(), status: z.string() }) - .strict(); + .passthrough(); const ComponentUptimeSummaryDto = z .object({ day: z.number().nullish(), @@ -1825,14 +1825,14 @@ const ComponentUptimeSummaryDto = z month: z.number().nullish(), source: z.string(), }) - .strict(); + .passthrough(); const CursorPageCheckResultDto = z .object({ data: z.array(CheckResultDto), nextCursor: z.string().nullish(), hasMore: z.boolean(), }) - .strict(); + .passthrough(); const ServiceCatalogDto = z .object({ id: z.string().uuid(), @@ -1855,14 +1855,14 @@ const ServiceCatalogDto = z dataCompleteness: z.string(), uptime30d: z.number().nullish(), }) - .strict(); + .passthrough(); const CursorPageServiceCatalogDto = z .object({ data: z.array(ServiceCatalogDto), nextCursor: z.string().nullish(), hasMore: z.boolean(), }) - .strict(); + .passthrough(); const ServicePollResultDto = z .object({ serviceId: z.string().uuid(), @@ -1875,14 +1875,14 @@ const ServicePollResultDto = z componentCount: z.number().int(), degradedCount: z.number().int(), }) - .strict(); + .passthrough(); const CursorPageServicePollResultDto = z .object({ data: z.array(ServicePollResultDto), nextCursor: z.string().nullish(), hasMore: z.boolean(), }) - .strict(); + .passthrough(); const MonitorsSummaryDto = z .object({ total: z.number().int(), @@ -1893,17 +1893,17 @@ const MonitorsSummaryDto = z avgUptime24h: z.number().nullish(), avgUptime30d: z.number().nullish(), }) - .strict(); + .passthrough(); const IncidentsSummaryDto = z .object({ active: z.number().int(), resolvedToday: z.number().int(), mttr30d: z.number().nullish(), }) - .strict(); + .passthrough(); const DashboardOverviewDto = z .object({ monitors: MonitorsSummaryDto, incidents: IncidentsSummaryDto }) - .strict(); + .passthrough(); const DayIncident = z .object({ id: z.string().uuid(), @@ -1924,7 +1924,7 @@ const DekRotationResultDto = z channelsReEncrypted: z.number().int(), rotatedAt: z.string().datetime({ offset: true }), }) - .strict(); + .passthrough(); const DeleteChannelResult = z .object({ affectedPolicies: z.number().int(), @@ -1946,7 +1946,7 @@ const DeliveryAttemptDto = z requestHeaders: z.record(z.string(), z.string().nullable()).nullish(), attemptedAt: z.string().datetime({ offset: true }), }) - .strict(); + .passthrough(); const DeployLockDto = z .object({ id: z.string().uuid(), @@ -1954,7 +1954,7 @@ const DeployLockDto = z lockedAt: z.string().datetime({ offset: true }), expiresAt: z.string().datetime({ offset: true }), }) - .strict(); + .passthrough(); const EnvironmentDto = z .object({ id: z.string().uuid(), @@ -1967,7 +1967,7 @@ const EnvironmentDto = z monitorCount: z.number().int(), isDefault: z.boolean(), }) - .strict(); + .passthrough(); const GlobalStatusSummaryDto = z .object({ totalServices: z.number().int(), @@ -1980,8 +1980,8 @@ const GlobalStatusSummaryDto = z activeIncidentCount: z.number().int(), servicesWithIssues: z.array(ServiceCatalogDto), }) - .strict(); -const HeartbeatPingResponse = z.object({ ok: z.boolean() }).strict(); + .passthrough(); +const HeartbeatPingResponse = z.object({ ok: z.boolean() }).passthrough(); const IncidentDto = z .object({ id: z.string().uuid(), @@ -2019,7 +2019,7 @@ const IncidentDto = z triggeredByRuleIndex: z.number().int().nullish(), engineVersion: z.string().nullish(), }) - .strict(); + .passthrough(); const IncidentUpdateDto = z .object({ id: z.string().uuid(), @@ -2031,7 +2031,7 @@ const IncidentUpdateDto = z notifySubscribers: z.boolean(), createdAt: z.string().datetime({ offset: true }), }) - .strict(); + .passthrough(); const LinkedStatusPageIncidentDto = z .object({ id: z.string().uuid(), @@ -2044,14 +2044,14 @@ const LinkedStatusPageIncidentDto = z scheduled: z.boolean(), publishedAt: z.string().datetime({ offset: true }).nullish(), }) - .strict(); + .passthrough(); const IncidentDetailDto = z .object({ incident: IncidentDto, updates: z.array(IncidentUpdateDto), statusPageIncidents: z.array(LinkedStatusPageIncidentDto).nullish(), }) - .strict(); + .passthrough(); const IncidentFilterParams = z .object({ status: z @@ -2090,14 +2090,14 @@ const IncidentPolicyDto = z monitorRegionCount: z.number().int().nullish(), checkFrequencySeconds: z.number().int().nullish(), }) - .strict(); + .passthrough(); const IncidentTimelineDto = z .object({ transitions: z.array(IncidentStateTransitionDto), triggeringEvaluations: z.array(RuleEvaluationDto), policySnapshot: PolicySnapshotDto.nullish(), }) - .strict(); + .passthrough(); const IntegrationFieldDto = z .object({ key: z.string(), @@ -2110,13 +2110,13 @@ const IntegrationFieldDto = z options: z.array(z.string()).nullish(), default: z.string().nullish(), }) - .strict(); + .passthrough(); const IntegrationConfigSchemaDto = z .object({ connectionFields: z.array(IntegrationFieldDto), channelFields: z.array(IntegrationFieldDto), }) - .strict(); + .passthrough(); const IntegrationDto = z .object({ type: z.string(), @@ -2129,7 +2129,7 @@ const IntegrationDto = z setupGuideUrl: z.string(), configSchema: IntegrationConfigSchemaDto, }) - .strict(); + .passthrough(); const InviteDto = z .object({ inviteId: z.number().int(), @@ -2139,7 +2139,7 @@ const InviteDto = z consumedAt: z.string().datetime({ offset: true }).nullish(), revokedAt: z.string().datetime({ offset: true }).nullish(), }) - .strict(); + .passthrough(); const MaintenanceComponentRef = z .object({ id: z.string().uuid(), name: z.string(), status: z.string() }) .strict(); @@ -2150,7 +2150,7 @@ const MaintenanceUpdateDto = z body: z.string().nullish(), displayAt: z.string().datetime({ offset: true }).nullish(), }) - .strict(); + .passthrough(); const MaintenanceWindowDto = z .object({ id: z.string().uuid(), @@ -2163,7 +2163,7 @@ const MaintenanceWindowDto = z suppressAlerts: z.boolean(), createdAt: z.string().datetime({ offset: true }), }) - .strict(); + .passthrough(); const MemberDto = z .object({ userId: z.number().int(), @@ -2173,7 +2173,7 @@ const MemberDto = z status: z.string(), createdAt: z.string().datetime({ offset: true }), }) - .strict(); + .passthrough(); const MonitorAssertionDto = z .object({ id: z.string().uuid(), @@ -2225,7 +2225,7 @@ const MonitorAssertionDto = z ]), severity: z.string(), }) - .strict(); + .passthrough(); const MonitorAuthDto = z .object({ id: z.string().uuid(), @@ -2233,7 +2233,7 @@ const MonitorAuthDto = z authType: z.string(), config: z.discriminatedUnion("type", [ApiKeyAuthConfig, BasicAuthConfig, BearerAuthConfig, HeaderAuthConfig]), }) - .strict(); + .passthrough(); const TagDto = z .object({ id: z.string().uuid(), @@ -2243,7 +2243,7 @@ const TagDto = z createdAt: z.string().datetime({ offset: true }), updatedAt: z.string().datetime({ offset: true }), }) - .strict(); + .passthrough(); const Summary = z .object({ id: z.string().uuid(), @@ -2280,7 +2280,7 @@ const MonitorDto = z alertChannelIds: z.array(z.string().uuid()).nullish(), currentStatus: z.string().nullish(), }) - .strict(); + .passthrough(); const MonitorReference = z .object({ id: z.string().uuid(), name: z.string() }) .strict(); @@ -2300,7 +2300,7 @@ const MonitorTestResultDto = z assertionResults: z.array(AssertionTestResultDto), warnings: z.array(z.string()).nullish(), }) - .strict(); + .passthrough(); const MonitorVersionDto = z .object({ id: z.string().uuid(), @@ -2312,7 +2312,7 @@ const MonitorVersionDto = z changeSummary: z.string().nullish(), createdAt: z.string().datetime({ offset: true }), }) - .strict(); + .passthrough(); const NotificationDispatchDto = z .object({ id: z.string().uuid(), @@ -2330,7 +2330,7 @@ const NotificationDispatchDto = z createdAt: z.string().datetime({ offset: true }), updatedAt: z.string().datetime({ offset: true }), }) - .strict(); + .passthrough(); const NotificationDto = z .object({ id: z.number().int(), @@ -2342,7 +2342,7 @@ const NotificationDto = z read: z.boolean(), createdAt: z.string().datetime({ offset: true }), }) - .strict(); + .passthrough(); const NotificationPolicyDto = z .object({ id: z.string().uuid(), @@ -2355,7 +2355,7 @@ const NotificationPolicyDto = z createdAt: z.string().datetime({ offset: true }), updatedAt: z.string().datetime({ offset: true }), }) - .strict(); + .passthrough(); const OrganizationDto = z .object({ id: z.number().int(), @@ -2365,7 +2365,7 @@ const OrganizationDto = z industry: z.string().nullish(), websiteUrl: z.string().nullish(), }) - .strict(); + .passthrough(); const Pageable = z .object({ page: z.number().int().gte(0), @@ -2380,7 +2380,7 @@ const PollChartBucketDto = z avgResponseTimeMs: z.number().nullish(), totalPolls: z.number().int(), }) - .strict(); + .passthrough(); const RegionStatusDto = z .object({ region: z.string(), @@ -2389,7 +2389,7 @@ const RegionStatusDto = z timestamp: z.string().datetime({ offset: true }), severityHint: z.string().nullish(), }) - .strict(); + .passthrough(); const ResourceGroupHealthDto = z .object({ status: z.string(), @@ -2399,7 +2399,7 @@ const ResourceGroupHealthDto = z thresholdStatus: z.string().nullish(), failingCount: z.number().int().nullish(), }) - .strict(); + .passthrough(); const ResourceGroupMemberDto = z .object({ id: z.string().uuid(), @@ -2421,7 +2421,7 @@ const ResourceGroupMemberDto = z monitorType: z.string().nullish(), environmentName: z.string().nullish(), }) - .strict(); + .passthrough(); const ResourceGroupDto = z .object({ id: z.string().uuid(), @@ -2446,7 +2446,7 @@ const ResourceGroupDto = z createdAt: z.string().datetime({ offset: true }), updatedAt: z.string().datetime({ offset: true }), }) - .strict(); + .passthrough(); const ResultSummaryDto = z .object({ currentStatus: z.string(), @@ -2455,7 +2455,7 @@ const ResultSummaryDto = z uptime24h: z.number().nullish(), uptimeWindow: z.number().nullish(), }) - .strict(); + .passthrough(); const ScheduledMaintenanceDto = z .object({ id: z.string().uuid(), @@ -2471,7 +2471,7 @@ const ScheduledMaintenanceDto = z affectedComponents: z.array(MaintenanceComponentRef), updates: z.array(MaintenanceUpdateDto), }) - .strict(); + .passthrough(); const SecretDto = z .object({ id: z.string().uuid(), @@ -2482,7 +2482,7 @@ const SecretDto = z updatedAt: z.string().datetime({ offset: true }), usedByMonitors: z.array(MonitorReference).nullish(), }) - .strict(); + .passthrough(); const SeoMetadataDto = z .object({ shortDescription: z.string().nullable(), @@ -2490,7 +2490,7 @@ const SeoMetadataDto = z about: z.string().nullable(), }) .partial() - .strict(); + .passthrough(); const ServiceComponentDto = z .object({ id: z.string().uuid(), @@ -2517,7 +2517,7 @@ const ServiceComponentDto = z lastSeenAt: z.string().datetime({ offset: true }), isGroup: z.boolean(), }) - .strict(); + .passthrough(); const ServiceDayDetailDto = z .object({ date: z.string(), @@ -2528,13 +2528,13 @@ const ServiceDayDetailDto = z components: z.array(ComponentImpact), incidents: z.array(DayIncident), }) - .strict(); + .passthrough(); const ServiceStatusDto = z .object({ overallStatus: z.string(), lastPolledAt: z.string().datetime({ offset: true }).nullish(), }) - .strict(); + .passthrough(); const ServiceIncidentDto = z .object({ id: z.string().uuid(), @@ -2552,7 +2552,7 @@ const ServiceIncidentDto = z detectedAt: z.string().datetime({ offset: true }).nullish(), vendorCreatedAt: z.string().datetime({ offset: true }).nullish(), }) - .strict(); + .passthrough(); const ServiceDetailDto = z .object({ id: z.string().uuid(), @@ -2578,14 +2578,14 @@ const ServiceDetailDto = z seoMetadata: SeoMetadataDto.nullish(), relatedServices: z.array(ServiceCatalogDto).nullish(), }) - .strict(); + .passthrough(); const ServiceIncidentUpdateDto = z .object({ status: z.string(), body: z.string().nullish(), displayAt: z.string().datetime({ offset: true }).nullish(), }) - .strict(); + .passthrough(); const ServiceIncidentDetailDto = z .object({ id: z.string().uuid(), @@ -2599,7 +2599,7 @@ const ServiceIncidentDetailDto = z affectedComponents: z.array(z.string()).nullish(), updates: z.array(ServiceIncidentUpdateDto), }) - .strict(); + .passthrough(); const ServiceLiveStatusDto = z .object({ overallStatus: z.string().nullish(), @@ -2607,7 +2607,7 @@ const ServiceLiveStatusDto = z activeIncidentCount: z.number().int(), lastPolledAt: z.string().nullish(), }) - .strict(); + .passthrough(); const ServicePollSummaryDto = z .object({ uptimePercentage: z.number().nullish(), @@ -2618,7 +2618,7 @@ const ServicePollSummaryDto = z window: z.string(), chartData: z.array(PollChartBucketDto), }) - .strict(); + .passthrough(); const ServiceSubscriptionDto = z .object({ subscriptionId: z.string().uuid(), @@ -2637,14 +2637,14 @@ const ServiceSubscriptionDto = z alertSensitivity: z.string().min(1), subscribedAt: z.string().datetime({ offset: true }), }) - .strict(); + .passthrough(); const UptimeBucketDto = z .object({ timestamp: z.string().datetime({ offset: true }), uptimePct: z.number().nullish(), totalPolls: z.number().int(), }) - .strict(); + .passthrough(); const ServiceUptimeResponse = z .object({ overallUptimePct: z.number().nullish(), @@ -2653,120 +2653,120 @@ const ServiceUptimeResponse = z buckets: z.array(UptimeBucketDto), source: z.string().nullish(), }) - .strict(); + .passthrough(); const SingleValueResponseAlertChannelDto = z .object({ data: AlertChannelDto }) - .strict(); + .passthrough(); const SingleValueResponseAlertDeliveryDto = z .object({ data: AlertDeliveryDto }) - .strict(); + .passthrough(); const SingleValueResponseApiKeyCreateResponse = z .object({ data: ApiKeyCreateResponse }) - .strict(); -const SingleValueResponseApiKeyDto = z.object({ data: ApiKeyDto }).strict(); + .passthrough(); +const SingleValueResponseApiKeyDto = z.object({ data: ApiKeyDto }).passthrough(); const SingleValueResponseAuthMeResponse = z .object({ data: AuthMeResponse }) - .strict(); + .passthrough(); const SingleValueResponseBatchComponentUptimeDto = z .object({ data: BatchComponentUptimeDto }) - .strict(); + .passthrough(); const SingleValueResponseBulkMonitorActionResult = z .object({ data: BulkMonitorActionResult }) - .strict(); + .passthrough(); const SingleValueResponseCheckTraceDto = z .object({ data: CheckTraceDto }) - .strict(); + .passthrough(); const SingleValueResponseDashboardOverviewDto = z .object({ data: DashboardOverviewDto }) - .strict(); + .passthrough(); const SingleValueResponseDekRotationResultDto = z .object({ data: DekRotationResultDto }) - .strict(); + .passthrough(); const SingleValueResponseDeployLockDto = z .object({ data: DeployLockDto }) - .strict(); + .passthrough(); const SingleValueResponseEnvironmentDto = z .object({ data: EnvironmentDto }) - .strict(); + .passthrough(); const SingleValueResponseGlobalStatusSummaryDto = z .object({ data: GlobalStatusSummaryDto }) - .strict(); + .passthrough(); const SingleValueResponseIncidentDetailDto = z .object({ data: IncidentDetailDto }) - .strict(); + .passthrough(); const SingleValueResponseIncidentPolicyDto = z .object({ data: IncidentPolicyDto }) - .strict(); + .passthrough(); const SingleValueResponseIncidentTimelineDto = z .object({ data: IncidentTimelineDto }) - .strict(); -const SingleValueResponseInviteDto = z.object({ data: InviteDto }).strict(); + .passthrough(); +const SingleValueResponseInviteDto = z.object({ data: InviteDto }).passthrough(); const SingleValueResponseListUUID = z .object({ data: z.array(z.string().uuid()) }) - .strict(); -const SingleValueResponseLong = z.object({ data: z.number().int() }).strict(); + .passthrough(); +const SingleValueResponseLong = z.object({ data: z.number().int() }).passthrough(); const SingleValueResponseMaintenanceWindowDto = z .object({ data: MaintenanceWindowDto }) - .strict(); + .passthrough(); const SingleValueResponseMonitorAssertionDto = z .object({ data: MonitorAssertionDto }) - .strict(); + .passthrough(); const SingleValueResponseMonitorAuthDto = z .object({ data: MonitorAuthDto }) - .strict(); -const SingleValueResponseMonitorDto = z.object({ data: MonitorDto }).strict(); + .passthrough(); +const SingleValueResponseMonitorDto = z.object({ data: MonitorDto }).passthrough(); const SingleValueResponseMonitorTestResultDto = z .object({ data: MonitorTestResultDto }) - .strict(); + .passthrough(); const SingleValueResponseMonitorVersionDto = z .object({ data: MonitorVersionDto }) - .strict(); + .passthrough(); const SingleValueResponseNotificationDispatchDto = z .object({ data: NotificationDispatchDto }) - .strict(); + .passthrough(); const SingleValueResponseNotificationPolicyDto = z .object({ data: NotificationPolicyDto }) - .strict(); + .passthrough(); const SingleValueResponseOrganizationDto = z .object({ data: OrganizationDto }) - .strict(); + .passthrough(); const SingleValueResponsePolicySnapshotDto = z .object({ data: PolicySnapshotDto.nullable() }) - .strict(); + .passthrough(); const SingleValueResponseResourceGroupDto = z .object({ data: ResourceGroupDto }) - .strict(); + .passthrough(); const SingleValueResponseResourceGroupHealthDto = z .object({ data: ResourceGroupHealthDto }) - .strict(); + .passthrough(); const SingleValueResponseResourceGroupMemberDto = z .object({ data: ResourceGroupMemberDto }) - .strict(); + .passthrough(); const SingleValueResponseResultSummaryDto = z .object({ data: ResultSummaryDto }) - .strict(); -const SingleValueResponseSecretDto = z.object({ data: SecretDto }).strict(); + .passthrough(); +const SingleValueResponseSecretDto = z.object({ data: SecretDto }).passthrough(); const SingleValueResponseServiceDayDetailDto = z .object({ data: ServiceDayDetailDto }) - .strict(); + .passthrough(); const SingleValueResponseServiceDetailDto = z .object({ data: ServiceDetailDto }) - .strict(); + .passthrough(); const SingleValueResponseServiceIncidentDetailDto = z .object({ data: ServiceIncidentDetailDto }) - .strict(); + .passthrough(); const SingleValueResponseServiceLiveStatusDto = z .object({ data: ServiceLiveStatusDto }) - .strict(); + .passthrough(); const SingleValueResponseServicePollSummaryDto = z .object({ data: ServicePollSummaryDto }) - .strict(); + .passthrough(); const SingleValueResponseServiceSubscriptionDto = z .object({ data: ServiceSubscriptionDto }) - .strict(); + .passthrough(); const SingleValueResponseServiceUptimeResponse = z .object({ data: ServiceUptimeResponse }) - .strict(); + .passthrough(); const StatusPageComponentDto = z .object({ id: z.string().uuid(), @@ -2786,10 +2786,10 @@ const StatusPageComponentDto = z createdAt: z.string().datetime({ offset: true }), updatedAt: z.string().datetime({ offset: true }), }) - .strict(); + .passthrough(); const SingleValueResponseStatusPageComponentDto = z .object({ data: StatusPageComponentDto }) - .strict(); + .passthrough(); const StatusPageComponentGroupDto = z .object({ id: z.string().uuid(), @@ -2803,10 +2803,10 @@ const StatusPageComponentGroupDto = z createdAt: z.string().datetime({ offset: true }), updatedAt: z.string().datetime({ offset: true }), }) - .strict(); + .passthrough(); const SingleValueResponseStatusPageComponentGroupDto = z .object({ data: StatusPageComponentGroupDto }) - .strict(); + .passthrough(); const StatusPageCustomDomainDto = z .object({ id: z.string().uuid(), @@ -2824,10 +2824,10 @@ const StatusPageCustomDomainDto = z updatedAt: z.string().datetime({ offset: true }), primary: z.boolean(), }) - .strict(); + .passthrough(); const SingleValueResponseStatusPageCustomDomainDto = z .object({ data: StatusPageCustomDomainDto }) - .strict(); + .passthrough(); const StatusPageDto = z .object({ id: z.string().uuid(), @@ -2847,17 +2847,17 @@ const StatusPageDto = z createdAt: z.string().datetime({ offset: true }), updatedAt: z.string().datetime({ offset: true }), }) - .strict(); + .passthrough(); const SingleValueResponseStatusPageDto = z .object({ data: StatusPageDto }) - .strict(); + .passthrough(); const StatusPageIncidentComponentDto = z .object({ statusPageComponentId: z.string().uuid(), componentStatus: z.string(), componentName: z.string(), }) - .strict(); + .passthrough(); const StatusPageIncidentUpdateDto = z .object({ id: z.string().uuid(), @@ -2868,7 +2868,7 @@ const StatusPageIncidentUpdateDto = z notifySubscribers: z.boolean(), createdAt: z.string().datetime({ offset: true }), }) - .strict(); + .passthrough(); const StatusPageIncidentDto = z .object({ id: z.string().uuid(), @@ -2892,10 +2892,10 @@ const StatusPageIncidentDto = z createdAt: z.string().datetime({ offset: true }), updatedAt: z.string().datetime({ offset: true }), }) - .strict(); + .passthrough(); const SingleValueResponseStatusPageIncidentDto = z .object({ data: StatusPageIncidentDto }) - .strict(); + .passthrough(); const StatusPageSubscriberDto = z .object({ id: z.string().uuid(), @@ -2903,18 +2903,18 @@ const StatusPageSubscriberDto = z confirmed: z.boolean(), createdAt: z.string().datetime({ offset: true }), }) - .strict(); + .passthrough(); const SingleValueResponseStatusPageSubscriberDto = z .object({ data: StatusPageSubscriberDto }) - .strict(); -const SingleValueResponseString = z.object({ data: z.string() }).strict(); -const SingleValueResponseTagDto = z.object({ data: TagDto }).strict(); + .passthrough(); +const SingleValueResponseString = z.object({ data: z.string() }).passthrough(); +const SingleValueResponseTagDto = z.object({ data: TagDto }).passthrough(); const TestChannelResult = z .object({ success: z.boolean(), message: z.string() }) .strict(); const SingleValueResponseTestChannelResult = z .object({ data: TestChannelResult }) - .strict(); + .passthrough(); const TestMatchResult = z .object({ matched: z.boolean(), @@ -2924,7 +2924,7 @@ const TestMatchResult = z .strict(); const SingleValueResponseTestMatchResult = z .object({ data: TestMatchResult }) - .strict(); + .passthrough(); const UptimeDto = z .object({ uptimePercentage: z.number().nullish(), @@ -2933,8 +2933,8 @@ const UptimeDto = z avgLatencyMs: z.number().nullish(), p95LatencyMs: z.number().nullish(), }) - .strict(); -const SingleValueResponseUptimeDto = z.object({ data: UptimeDto }).strict(); + .passthrough(); +const SingleValueResponseUptimeDto = z.object({ data: UptimeDto }).passthrough(); const WebhookEndpointDto = z .object({ id: z.string().uuid(), @@ -2948,16 +2948,16 @@ const WebhookEndpointDto = z createdAt: z.string().datetime({ offset: true }), updatedAt: z.string().datetime({ offset: true }), }) - .strict(); + .passthrough(); const SingleValueResponseWebhookEndpointDto = z .object({ data: WebhookEndpointDto }) - .strict(); + .passthrough(); const WebhookSigningSecretDto = z .object({ configured: z.boolean(), maskedSecret: z.string().nullish() }) - .strict(); + .passthrough(); const SingleValueResponseWebhookSigningSecretDto = z .object({ data: WebhookSigningSecretDto }) - .strict(); + .passthrough(); const WebhookTestResult = z .object({ success: z.boolean(), @@ -2968,7 +2968,7 @@ const WebhookTestResult = z .strict(); const SingleValueResponseWebhookTestResult = z .object({ data: WebhookTestResult }) - .strict(); + .passthrough(); const WorkspaceDto = z .object({ id: z.number().int(), @@ -2977,10 +2977,10 @@ const WorkspaceDto = z name: z.string().min(1), orgId: z.number().int(), }) - .strict(); + .passthrough(); const SingleValueResponseWorkspaceDto = z .object({ data: WorkspaceDto }) - .strict(); + .passthrough(); const TableValueResultAlertChannelDto = z .object({ data: z.array(AlertChannelDto), @@ -2989,7 +2989,7 @@ const TableValueResultAlertChannelDto = z totalElements: z.number().int().nullish(), totalPages: z.number().int().nullish(), }) - .strict(); + .passthrough(); const TableValueResultAlertDeliveryDto = z .object({ data: z.array(AlertDeliveryDto), @@ -2998,7 +2998,7 @@ const TableValueResultAlertDeliveryDto = z totalElements: z.number().int().nullish(), totalPages: z.number().int().nullish(), }) - .strict(); + .passthrough(); const TableValueResultApiKeyDto = z .object({ data: z.array(ApiKeyDto), @@ -3007,7 +3007,7 @@ const TableValueResultApiKeyDto = z totalElements: z.number().int().nullish(), totalPages: z.number().int().nullish(), }) - .strict(); + .passthrough(); const TableValueResultAuditEventDto = z .object({ data: z.array(AuditEventDto), @@ -3016,7 +3016,7 @@ const TableValueResultAuditEventDto = z totalElements: z.number().int().nullish(), totalPages: z.number().int().nullish(), }) - .strict(); + .passthrough(); const TableValueResultCategoryDto = z .object({ data: z.array(CategoryDto), @@ -3025,7 +3025,7 @@ const TableValueResultCategoryDto = z totalElements: z.number().int().nullish(), totalPages: z.number().int().nullish(), }) - .strict(); + .passthrough(); const TableValueResultComponentUptimeDayDto = z .object({ data: z.array(ComponentUptimeDayDto), @@ -3034,7 +3034,7 @@ const TableValueResultComponentUptimeDayDto = z totalElements: z.number().int().nullish(), totalPages: z.number().int().nullish(), }) - .strict(); + .passthrough(); const TableValueResultDeliveryAttemptDto = z .object({ data: z.array(DeliveryAttemptDto), @@ -3043,7 +3043,7 @@ const TableValueResultDeliveryAttemptDto = z totalElements: z.number().int().nullish(), totalPages: z.number().int().nullish(), }) - .strict(); + .passthrough(); const TableValueResultEnvironmentDto = z .object({ data: z.array(EnvironmentDto), @@ -3052,7 +3052,7 @@ const TableValueResultEnvironmentDto = z totalElements: z.number().int().nullish(), totalPages: z.number().int().nullish(), }) - .strict(); + .passthrough(); const TableValueResultIncidentDto = z .object({ data: z.array(IncidentDto), @@ -3061,7 +3061,7 @@ const TableValueResultIncidentDto = z totalElements: z.number().int().nullish(), totalPages: z.number().int().nullish(), }) - .strict(); + .passthrough(); const TableValueResultIncidentStateTransitionDto = z .object({ data: z.array(IncidentStateTransitionDto), @@ -3070,7 +3070,7 @@ const TableValueResultIncidentStateTransitionDto = z totalElements: z.number().int().nullish(), totalPages: z.number().int().nullish(), }) - .strict(); + .passthrough(); const TableValueResultIntegrationDto = z .object({ data: z.array(IntegrationDto), @@ -3079,7 +3079,7 @@ const TableValueResultIntegrationDto = z totalElements: z.number().int().nullish(), totalPages: z.number().int().nullish(), }) - .strict(); + .passthrough(); const TableValueResultInviteDto = z .object({ data: z.array(InviteDto), @@ -3088,7 +3088,7 @@ const TableValueResultInviteDto = z totalElements: z.number().int().nullish(), totalPages: z.number().int().nullish(), }) - .strict(); + .passthrough(); const TableValueResultMaintenanceWindowDto = z .object({ data: z.array(MaintenanceWindowDto), @@ -3097,7 +3097,7 @@ const TableValueResultMaintenanceWindowDto = z totalElements: z.number().int().nullish(), totalPages: z.number().int().nullish(), }) - .strict(); + .passthrough(); const TableValueResultMemberDto = z .object({ data: z.array(MemberDto), @@ -3106,7 +3106,7 @@ const TableValueResultMemberDto = z totalElements: z.number().int().nullish(), totalPages: z.number().int().nullish(), }) - .strict(); + .passthrough(); const TableValueResultMonitorDto = z .object({ data: z.array(MonitorDto), @@ -3115,7 +3115,7 @@ const TableValueResultMonitorDto = z totalElements: z.number().int().nullish(), totalPages: z.number().int().nullish(), }) - .strict(); + .passthrough(); const TableValueResultMonitorVersionDto = z .object({ data: z.array(MonitorVersionDto), @@ -3124,7 +3124,7 @@ const TableValueResultMonitorVersionDto = z totalElements: z.number().int().nullish(), totalPages: z.number().int().nullish(), }) - .strict(); + .passthrough(); const TableValueResultNotificationDispatchDto = z .object({ data: z.array(NotificationDispatchDto), @@ -3133,7 +3133,7 @@ const TableValueResultNotificationDispatchDto = z totalElements: z.number().int().nullish(), totalPages: z.number().int().nullish(), }) - .strict(); + .passthrough(); const TableValueResultNotificationDto = z .object({ data: z.array(NotificationDto), @@ -3142,7 +3142,7 @@ const TableValueResultNotificationDto = z totalElements: z.number().int().nullish(), totalPages: z.number().int().nullish(), }) - .strict(); + .passthrough(); const TableValueResultNotificationPolicyDto = z .object({ data: z.array(NotificationPolicyDto), @@ -3151,7 +3151,7 @@ const TableValueResultNotificationPolicyDto = z totalElements: z.number().int().nullish(), totalPages: z.number().int().nullish(), }) - .strict(); + .passthrough(); const TableValueResultResourceGroupDto = z .object({ data: z.array(ResourceGroupDto), @@ -3160,7 +3160,7 @@ const TableValueResultResourceGroupDto = z totalElements: z.number().int().nullish(), totalPages: z.number().int().nullish(), }) - .strict(); + .passthrough(); const TableValueResultRuleEvaluationDto = z .object({ data: z.array(RuleEvaluationDto), @@ -3169,7 +3169,7 @@ const TableValueResultRuleEvaluationDto = z totalElements: z.number().int().nullish(), totalPages: z.number().int().nullish(), }) - .strict(); + .passthrough(); const TableValueResultScheduledMaintenanceDto = z .object({ data: z.array(ScheduledMaintenanceDto), @@ -3178,7 +3178,7 @@ const TableValueResultScheduledMaintenanceDto = z totalElements: z.number().int().nullish(), totalPages: z.number().int().nullish(), }) - .strict(); + .passthrough(); const TableValueResultSecretDto = z .object({ data: z.array(SecretDto), @@ -3187,7 +3187,7 @@ const TableValueResultSecretDto = z totalElements: z.number().int().nullish(), totalPages: z.number().int().nullish(), }) - .strict(); + .passthrough(); const TableValueResultServiceComponentDto = z .object({ data: z.array(ServiceComponentDto), @@ -3196,7 +3196,7 @@ const TableValueResultServiceComponentDto = z totalElements: z.number().int().nullish(), totalPages: z.number().int().nullish(), }) - .strict(); + .passthrough(); const TableValueResultServiceIncidentDto = z .object({ data: z.array(ServiceIncidentDto), @@ -3205,7 +3205,7 @@ const TableValueResultServiceIncidentDto = z totalElements: z.number().int().nullish(), totalPages: z.number().int().nullish(), }) - .strict(); + .passthrough(); const TableValueResultServiceSubscriptionDto = z .object({ data: z.array(ServiceSubscriptionDto), @@ -3214,7 +3214,7 @@ const TableValueResultServiceSubscriptionDto = z totalElements: z.number().int().nullish(), totalPages: z.number().int().nullish(), }) - .strict(); + .passthrough(); const TableValueResultStatusPageComponentDto = z .object({ data: z.array(StatusPageComponentDto), @@ -3223,7 +3223,7 @@ const TableValueResultStatusPageComponentDto = z totalElements: z.number().int().nullish(), totalPages: z.number().int().nullish(), }) - .strict(); + .passthrough(); const TableValueResultStatusPageComponentGroupDto = z .object({ data: z.array(StatusPageComponentGroupDto), @@ -3232,7 +3232,7 @@ const TableValueResultStatusPageComponentGroupDto = z totalElements: z.number().int().nullish(), totalPages: z.number().int().nullish(), }) - .strict(); + .passthrough(); const TableValueResultStatusPageCustomDomainDto = z .object({ data: z.array(StatusPageCustomDomainDto), @@ -3241,7 +3241,7 @@ const TableValueResultStatusPageCustomDomainDto = z totalElements: z.number().int().nullish(), totalPages: z.number().int().nullish(), }) - .strict(); + .passthrough(); const TableValueResultStatusPageDto = z .object({ data: z.array(StatusPageDto), @@ -3250,7 +3250,7 @@ const TableValueResultStatusPageDto = z totalElements: z.number().int().nullish(), totalPages: z.number().int().nullish(), }) - .strict(); + .passthrough(); const TableValueResultStatusPageIncidentDto = z .object({ data: z.array(StatusPageIncidentDto), @@ -3259,7 +3259,7 @@ const TableValueResultStatusPageIncidentDto = z totalElements: z.number().int().nullish(), totalPages: z.number().int().nullish(), }) - .strict(); + .passthrough(); const TableValueResultStatusPageSubscriberDto = z .object({ data: z.array(StatusPageSubscriberDto), @@ -3268,7 +3268,7 @@ const TableValueResultStatusPageSubscriberDto = z totalElements: z.number().int().nullish(), totalPages: z.number().int().nullish(), }) - .strict(); + .passthrough(); const TableValueResultTagDto = z .object({ data: z.array(TagDto), @@ -3277,7 +3277,7 @@ const TableValueResultTagDto = z totalElements: z.number().int().nullish(), totalPages: z.number().int().nullish(), }) - .strict(); + .passthrough(); const WebhookDeliveryDto = z .object({ id: z.string().uuid(), @@ -3295,7 +3295,7 @@ const WebhookDeliveryDto = z nextRetryAt: z.string().datetime({ offset: true }).nullish(), createdAt: z.string().datetime({ offset: true }), }) - .strict(); + .passthrough(); const TableValueResultWebhookDeliveryDto = z .object({ data: z.array(WebhookDeliveryDto), @@ -3304,7 +3304,7 @@ const TableValueResultWebhookDeliveryDto = z totalElements: z.number().int().nullish(), totalPages: z.number().int().nullish(), }) - .strict(); + .passthrough(); const TableValueResultWebhookEndpointDto = z .object({ data: z.array(WebhookEndpointDto), @@ -3313,7 +3313,7 @@ const TableValueResultWebhookEndpointDto = z totalElements: z.number().int().nullish(), totalPages: z.number().int().nullish(), }) - .strict(); + .passthrough(); const TableValueResultWorkspaceDto = z .object({ data: z.array(WorkspaceDto), @@ -3322,13 +3322,13 @@ const TableValueResultWorkspaceDto = z totalElements: z.number().int().nullish(), totalPages: z.number().int().nullish(), }) - .strict(); + .passthrough(); const WebhookEventCatalogEntry = z .object({ type: z.string(), surface: z.string(), description: z.string() }) .strict(); const WebhookEventCatalogResponse = z .object({ data: z.array(WebhookEventCatalogEntry) }) - .strict(); + .passthrough(); export const schemas = { pageable, From f089a08b80cb121630aa17dc1ad8a5a49fa6e505 Mon Sep 17 00:00:00 2001 From: caballeto Date: Tue, 16 Jun 2026 19:04:08 +0200 Subject: [PATCH 2/2] test: update schema strictness test for Postel's Law Response DTOs now use .passthrough() and should accept unknown fields. Request schemas still reject unknown fields (.strict()). Updated the test to assert both behaviors. Co-authored-by: Cursor --- test/schemas.test.ts | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/test/schemas.test.ts b/test/schemas.test.ts index 541e5fa..c0b3aea 100644 --- a/test/schemas.test.ts +++ b/test/schemas.test.ts @@ -484,16 +484,21 @@ describe('enum validation rejects unknown values', () => { }) }) -// ── Strict objects (no passthrough on generated DTOs) ────────────── -// Generated schemas emit `.strict()` so unknown fields are REJECTED -// (not silently stripped). This catches API schema drift at the parse -// boundary instead of allowing new server-side fields to vanish into -// consumer code that hasn't been updated yet. - -describe('strict object schemas reject unknown fields', () => { - it('StatusPageDto rejects unknown fields on parse', () => { +// ── Response DTOs use .passthrough() (Postel's Law) ───────────────── +// Response-shape schemas (.passthrough()) tolerate unknown fields so +// that new API fields don't break older SDK versions. Request schemas +// keep .strict() for typo detection. + +describe('response DTOs tolerate unknown fields (Postel\'s Law)', () => { + it('StatusPageDto accepts unknown fields without error', () => { const raw = {...validStatusPageDto, futureField: 'new-api-version-data'} const result = schemas.StatusPageDto.safeParse(raw) + expect(result.success).toBe(true) + }) + + it('CreateStatusPageRequest still rejects unknown fields', () => { + const raw = {name: 'Test', slug: 'test', typo: 'caught'} + const result = schemas.CreateStatusPageRequest.safeParse(raw) expect(result.success).toBe(false) if (!result.success) { expect(result.error.issues[0].code).toBe('unrecognized_keys')