From 45d2e6d914b07765fe5580163df0c0e3c83788b6 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 8 Jun 2026 05:39:20 +0000 Subject: [PATCH 1/2] feat(architecture): add prefer-schema-validation rule MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Flags hand-rolled runtime type/shape validation — a type-predicate / assertion function or a validator-named function (isUser, validateConfig, assertX) that checks an object's shape with two or more distinct typeof member checks — and recommends parsing with a schema validator (Zod, Valibot, Yup) instead. Only `typeof param.member === ""` checks count, so polymorphic dispatch on the parameter itself, serializers without a validator name, nested-function checks, and dynamic computed members stay quiet. Co-authored-by: Nisarg Patel --- .../src/plugin/rule-registry.ts | 12 + .../prefer-schema-validation.test.ts | 400 ++++++++++++++++++ .../architecture/prefer-schema-validation.ts | 246 +++++++++++ 3 files changed, 658 insertions(+) create mode 100644 packages/oxlint-plugin-react-doctor/src/plugin/rules/architecture/prefer-schema-validation.test.ts create mode 100644 packages/oxlint-plugin-react-doctor/src/plugin/rules/architecture/prefer-schema-validation.ts diff --git a/packages/oxlint-plugin-react-doctor/src/plugin/rule-registry.ts b/packages/oxlint-plugin-react-doctor/src/plugin/rule-registry.ts index 7f94a429d..e54bcbdbe 100644 --- a/packages/oxlint-plugin-react-doctor/src/plugin/rule-registry.ts +++ b/packages/oxlint-plugin-react-doctor/src/plugin/rule-registry.ts @@ -240,6 +240,7 @@ import { preferFunctionComponent } from "./rules/react-builtins/prefer-function- import { preferHtmlDialog } from "./rules/a11y/prefer-html-dialog.js"; import { preferModuleScopePureFunction } from "./rules/architecture/prefer-module-scope-pure-function.js"; import { preferModuleScopeStaticValue } from "./rules/architecture/prefer-module-scope-static-value.js"; +import { preferSchemaValidation } from "./rules/architecture/prefer-schema-validation.js"; import { preferStableEmptyFallback } from "./rules/performance/prefer-stable-empty-fallback.js"; import { preferTagOverRole } from "./rules/a11y/prefer-tag-over-role.js"; import { preferUseEffectEvent } from "./rules/state-and-effects/prefer-use-effect-event.js"; @@ -2900,6 +2901,17 @@ export const reactDoctorRules = [ category: "Maintainability", }, }, + { + key: "react-doctor/prefer-schema-validation", + id: "prefer-schema-validation", + source: "react-doctor", + originallyExternal: false, + rule: { + ...preferSchemaValidation, + framework: "global", + category: "Maintainability", + }, + }, { key: "react-doctor/prefer-stable-empty-fallback", id: "prefer-stable-empty-fallback", diff --git a/packages/oxlint-plugin-react-doctor/src/plugin/rules/architecture/prefer-schema-validation.test.ts b/packages/oxlint-plugin-react-doctor/src/plugin/rules/architecture/prefer-schema-validation.test.ts new file mode 100644 index 000000000..02c24c0cb --- /dev/null +++ b/packages/oxlint-plugin-react-doctor/src/plugin/rules/architecture/prefer-schema-validation.test.ts @@ -0,0 +1,400 @@ +import { describe, expect, it } from "vite-plus/test"; +import { runRule } from "../../../test-utils/run-rule.js"; +import { preferSchemaValidation } from "./prefer-schema-validation.js"; + +describe("prefer-schema-validation", () => { + describe("reports hand-rolled validation", () => { + it("flags a TS type guard with several typeof member checks", () => { + const result = runRule( + preferSchemaValidation, + ` + function isUser(value: unknown): value is User { + return ( + typeof value.id === "string" && + typeof value.name === "string" && + typeof value.age === "number" + ); + } + `, + { filename: "is-user.ts" }, + ); + + expect(result.parseErrors).toEqual([]); + expect(result.diagnostics).toHaveLength(1); + expect(result.diagnostics[0].message).toContain("isUser"); + expect(result.diagnostics[0].message).toContain("value"); + expect(result.diagnostics[0].message).toContain("3 `typeof` checks"); + expect(result.diagnostics[0].message).toContain("schema validator"); + }); + + it("flags an assertion function that throws on bad fields", () => { + const result = runRule( + preferSchemaValidation, + ` + function assertConfig(input: unknown): asserts input is Config { + if (typeof input.host !== "string") throw new Error("host"); + if (typeof input.port !== "number") throw new Error("port"); + } + `, + { filename: "assert-config.ts" }, + ); + + expect(result.diagnostics).toHaveLength(1); + expect(result.diagnostics[0].message).toContain("assertConfig"); + expect(result.diagnostics[0].message).toContain("2 `typeof` checks"); + }); + + it("flags an untyped validator-named arrow in plain JS", () => { + const result = runRule( + preferSchemaValidation, + ` + const isUser = (value) => + typeof value.id === "string" && typeof value.name === "string"; + `, + { filename: "is-user.js" }, + ); + + expect(result.diagnostics).toHaveLength(1); + expect(result.diagnostics[0].message).toContain("isUser"); + }); + + it("flags a validate*-named function declaration", () => { + const result = runRule( + preferSchemaValidation, + ` + function validateConfig(config) { + return ( + typeof config.host === "string" && + typeof config.port === "number" && + typeof config.secure === "boolean" + ); + } + `, + { filename: "validate-config.js" }, + ); + + expect(result.diagnostics).toHaveLength(1); + expect(result.diagnostics[0].message).toContain("validateConfig"); + }); + + it("flags an object method validator", () => { + const result = runRule( + preferSchemaValidation, + ` + const guards = { + isPoint(value) { + return typeof value.x === "number" && typeof value.y === "number"; + }, + }; + `, + { filename: "guards.js" }, + ); + + expect(result.diagnostics).toHaveLength(1); + expect(result.diagnostics[0].message).toContain("isPoint"); + }); + + it("flags a class method validator", () => { + const result = runRule( + preferSchemaValidation, + ` + class Parser { + validatePayload(payload) { + return typeof payload.id === "string" && typeof payload.kind === "string"; + } + } + `, + { filename: "parser.js" }, + ); + + expect(result.diagnostics).toHaveLength(1); + expect(result.diagnostics[0].message).toContain("validatePayload"); + }); + + it("flags a validator assigned to a member expression", () => { + const result = runRule( + preferSchemaValidation, + ` + const validators = {}; + validators.validateUser = (value) => + typeof value.id === "string" && typeof value.name === "string"; + `, + { filename: "validators.js" }, + ); + + expect(result.diagnostics).toHaveLength(1); + expect(result.diagnostics[0].message).toContain("validateUser"); + }); + + it("resolves the param root through `as` casts and parentheses", () => { + const result = runRule( + preferSchemaValidation, + ` + function isUser(value: unknown): value is User { + return ( + typeof (value as User).id === "string" && + typeof (value).name === "string" + ); + } + `, + { filename: "is-user.ts" }, + ); + + expect(result.diagnostics).toHaveLength(1); + expect(result.diagnostics[0].message).toContain("2 `typeof` checks"); + }); + + it("resolves the param root through optional chaining", () => { + const result = runRule( + preferSchemaValidation, + ` + function isUser(value: unknown): value is User { + return typeof value?.id === "string" && typeof value?.name === "string"; + } + `, + { filename: "is-user.ts" }, + ); + + expect(result.diagnostics).toHaveLength(1); + }); + + it("counts static string-computed members and nested property paths distinctly", () => { + const result = runRule( + preferSchemaValidation, + ` + function isAddress(value) { + return ( + typeof value["street"] === "string" && + typeof value.geo.lat === "number" && + typeof value.geo.lng === "number" + ); + } + `, + { filename: "is-address.js" }, + ); + + expect(result.diagnostics).toHaveLength(1); + expect(result.diagnostics[0].message).toContain("3 `typeof` checks"); + }); + + it("flags the idiomatic object guard that also checks `typeof value`", () => { + const result = runRule( + preferSchemaValidation, + ` + function isUser(value: unknown): value is User { + return ( + typeof value === "object" && + value !== null && + typeof value.id === "string" && + typeof value.name === "string" + ); + } + `, + { filename: "is-user.ts" }, + ); + + expect(result.diagnostics).toHaveLength(1); + // The bare `typeof value` dispatch is not a member check, so only the + // two member checks count. + expect(result.diagnostics[0].message).toContain("2 `typeof` checks"); + }); + + it("labels an anonymous type-guard arrow without a binding name", () => { + const result = runRule( + preferSchemaValidation, + ` + useGuard((value: unknown): value is User => + typeof value.id === "string" && typeof value.name === "string"); + `, + { filename: "use-guard.ts" }, + ); + + expect(result.diagnostics).toHaveLength(1); + expect(result.diagnostics[0].message).toContain("This type guard"); + }); + }); + + describe("stays quiet on valid code", () => { + it("does not flag a single typeof member check", () => { + const result = runRule( + preferSchemaValidation, + ` + function isCallable(options) { + return typeof options.onChange === "function"; + } + `, + { filename: "is-callable.js" }, + ); + + expect(result.diagnostics).toEqual([]); + }); + + it("does not flag polymorphic dispatch on the parameter itself", () => { + const result = runRule( + preferSchemaValidation, + ` + function format(value) { + if (typeof value === "string") return value; + if (typeof value === "number") return String(value); + if (typeof value === "boolean") return value ? "yes" : "no"; + return ""; + } + `, + { filename: "format.js" }, + ); + + expect(result.diagnostics).toEqual([]); + }); + + it("does not flag a non-validator function even with several member checks", () => { + // No type predicate and no validator-like name: a serializer that + // inspects runtime types is intentionally out of scope for v1. + const result = runRule( + preferSchemaValidation, + ` + function serialize(node) { + if (typeof node.value === "string") return node.value; + if (typeof node.count === "number") return String(node.count); + if (typeof node.flag === "boolean") return node.flag ? "1" : "0"; + return ""; + } + `, + { filename: "serialize.js" }, + ); + + expect(result.diagnostics).toEqual([]); + }); + + it("does not flag typeof checks rooted at a different object", () => { + const result = runRule( + preferSchemaValidation, + ` + function isSupported(value) { + return ( + typeof window.IntersectionObserver === "function" && + typeof navigator.serviceWorker === "object" + ); + } + `, + { filename: "is-supported.js" }, + ); + + expect(result.diagnostics).toEqual([]); + }); + + it("does not count typeof checks inside a nested function", () => { + const result = runRule( + preferSchemaValidation, + ` + function isReady(value) { + const inner = (other) => + typeof other.a === "string" && typeof other.b === "string"; + return typeof value.loaded === "boolean" && inner(value.meta); + } + `, + { filename: "is-ready.js" }, + ); + + expect(result.diagnostics).toEqual([]); + }); + + it("dedupes repeated checks on the same property (optional-field guard)", () => { + const result = runRule( + preferSchemaValidation, + ` + function isName(value) { + return typeof value.name === "string" || typeof value.name === "undefined"; + } + `, + { filename: "is-name.js" }, + ); + + expect(result.diagnostics).toEqual([]); + }); + + it("ignores comparisons against non-typeof string literals", () => { + const result = runRule( + preferSchemaValidation, + ` + function isAdmin(user) { + return typeof user.id === "string" && user.role === "admin"; + } + `, + { filename: "is-admin.js" }, + ); + + expect(result.diagnostics).toEqual([]); + }); + + it("does not over-count dynamic computed members", () => { + const result = runRule( + preferSchemaValidation, + ` + function isShape(value, keyA, keyB) { + return typeof value[keyA] === "string" && typeof value[keyB] === "number"; + } + `, + { filename: "is-shape.js" }, + ); + + expect(result.diagnostics).toEqual([]); + }); + + it("does not match names that merely start with validator letters", () => { + const result = runRule( + preferSchemaValidation, + ` + function island(value) { + return typeof value.lat === "number" && typeof value.lng === "number"; + } + `, + { filename: "island.js" }, + ); + + expect(result.diagnostics).toEqual([]); + }); + + it("does not flag destructured-parameter validators (v1 non-goal)", () => { + const result = runRule( + preferSchemaValidation, + ` + function isUser({ id, name }) { + return typeof id === "string" && typeof name === "string"; + } + `, + { filename: "is-user.js" }, + ); + + expect(result.diagnostics).toEqual([]); + }); + + it("does not flag a render-prop children check", () => { + const result = runRule( + preferSchemaValidation, + ` + function Panel({ children, title }) { + if (typeof children === "function") return children(title); + return children; + } + `, + { filename: "panel.tsx" }, + ); + + expect(result.diagnostics).toEqual([]); + }); + + it("does not flag code that already uses a schema validator", () => { + const result = runRule( + preferSchemaValidation, + ` + import { z } from "zod"; + const userSchema = z.object({ id: z.string(), name: z.string() }); + const parseUser = (value) => userSchema.parse(value); + `, + { filename: "user-schema.ts" }, + ); + + expect(result.diagnostics).toEqual([]); + }); + }); +}); diff --git a/packages/oxlint-plugin-react-doctor/src/plugin/rules/architecture/prefer-schema-validation.ts b/packages/oxlint-plugin-react-doctor/src/plugin/rules/architecture/prefer-schema-validation.ts new file mode 100644 index 000000000..b97f2087c --- /dev/null +++ b/packages/oxlint-plugin-react-doctor/src/plugin/rules/architecture/prefer-schema-validation.ts @@ -0,0 +1,246 @@ +import { defineRule } from "../../utils/define-rule.js"; +import { isFunctionLike } from "../../utils/is-function-like.js"; +import { isNodeOfType } from "../../utils/is-node-of-type.js"; +import { stripParenExpression } from "../../utils/strip-paren-expression.js"; +import { walkAst } from "../../utils/walk-ast.js"; +import { getStaticMemberPropertyName } from "../state-and-effects/utils/static-member-property-name.js"; +import type { EsTreeNode } from "../../utils/es-tree-node.js"; +import type { EsTreeNodeOfType } from "../../utils/es-tree-node-of-type.js"; +import type { Rule } from "../../utils/rule.js"; +import type { RuleContext } from "../../utils/rule-context.js"; + +// A function is flagged as hand-rolled validation only once it carries at +// least this many DISTINCT `typeof .` checks. Two is the +// floor for "validating the shape of an object" — a single `typeof` +// member check (`typeof options.onChange === "function"`) is ordinary +// optional-callback handling, not a schema. +const MINIMUM_TYPEOF_MEMBER_CHECKS = 2; + +// The eight runtime results of the `typeof` operator. Requiring the +// compared literal to be one of these keeps the detector on the genuine +// type-validation idiom and ignores incidental string comparisons. +const TYPEOF_RESULT_TAGS: ReadonlySet = new Set([ + "string", + "number", + "boolean", + "object", + "undefined", + "function", + "symbol", + "bigint", +]); + +const TYPE_COMPARISON_OPERATORS: ReadonlySet = new Set(["===", "!==", "==", "!="]); + +// Names that signal the author intends a function to validate a value's +// shape. `is`/`has`/`are` require an uppercase boundary so `island` or +// `haste` never match; the full-word prefixes are case-insensitive so +// PascalCase factory names (`ValidateInput`) still count. +const VALIDATOR_CAMEL_PREFIX_PATTERN = /^(is|has|are)[A-Z]/; +const VALIDATOR_WORD_PREFIX_PATTERN = /^(validate|assert|ensure|verify|guard|check)/i; + +const looksLikeValidatorName = (name: string): boolean => + VALIDATOR_CAMEL_PREFIX_PATTERN.test(name) || VALIDATOR_WORD_PREFIX_PATTERN.test(name); + +type FunctionLikeNode = + | EsTreeNodeOfType<"ArrowFunctionExpression"> + | EsTreeNodeOfType<"FunctionExpression"> + | EsTreeNodeOfType<"FunctionDeclaration">; + +// The `value is User` / `asserts value is User` annotation on a function's +// return type. Both forms are explicit declarations that the function's +// job is to validate the named parameter's type at runtime. +const getReturnTypePredicate = ( + functionNode: FunctionLikeNode, +): EsTreeNodeOfType<"TSTypePredicate"> | null => { + const returnType = functionNode.returnType; + if (!returnType || !isNodeOfType(returnType, "TSTypeAnnotation")) return null; + const annotation = returnType.typeAnnotation; + if (!isNodeOfType(annotation, "TSTypePredicate")) return null; + return annotation; +}; + +const getStaticPropertyKeyName = (key: EsTreeNode, computed: boolean): string | null => { + if (!computed && isNodeOfType(key, "Identifier")) return key.name; + if (computed && isNodeOfType(key, "Literal") && typeof key.value === "string") return key.value; + return null; +}; + +// Resolves the binding name a function is exposed under: a declaration / +// expression `id`, the `const name = …` it initializes, the object or +// class member it implements, or the assignment target it is assigned to. +const getFunctionBindingName = (functionNode: FunctionLikeNode): string | null => { + if ( + (isNodeOfType(functionNode, "FunctionDeclaration") || + isNodeOfType(functionNode, "FunctionExpression")) && + functionNode.id + ) { + return functionNode.id.name; + } + + const parent = functionNode.parent; + if (!parent) return null; + + if ( + isNodeOfType(parent, "VariableDeclarator") && + parent.init === functionNode && + isNodeOfType(parent.id, "Identifier") + ) { + return parent.id.name; + } + + if ( + (isNodeOfType(parent, "Property") || + isNodeOfType(parent, "PropertyDefinition") || + isNodeOfType(parent, "MethodDefinition")) && + parent.value === functionNode + ) { + return getStaticPropertyKeyName(parent.key, Boolean(parent.computed)); + } + + if (isNodeOfType(parent, "AssignmentExpression") && parent.right === functionNode) { + if (isNodeOfType(parent.left, "Identifier")) return parent.left.name; + return getStaticMemberPropertyName(parent.left); + } + + return null; +}; + +// The parameter whose shape the function validates: the parameter named +// by a type predicate when present, otherwise the first plain-identifier +// parameter. Destructured parameters are skipped — their members are +// already named bindings, not a single value being inspected. +const getValidatedParameterName = ( + functionNode: FunctionLikeNode, + predicate: EsTreeNodeOfType<"TSTypePredicate"> | null, +): string | null => { + if (predicate && isNodeOfType(predicate.parameterName, "Identifier")) { + return predicate.parameterName.name; + } + const firstParameter = functionNode.params?.[0]; + if (isNodeOfType(firstParameter, "Identifier")) return firstParameter.name; + return null; +}; + +const isTypeofOperand = ( + node: EsTreeNode | undefined, +): node is EsTreeNodeOfType<"UnaryExpression"> => + isNodeOfType(node, "UnaryExpression") && node.operator === "typeof"; + +const isTypeofResultLiteral = (node: EsTreeNode | undefined): boolean => + isNodeOfType(node, "Literal") && + typeof node.value === "string" && + TYPEOF_RESULT_TAGS.has(node.value); + +// For `typeof `, returns a stable per-property key when the +// expression is a member chain rooted at `validatedParameterName` +// (`value.id`, `value.address.city`, `value["id"]`). Returns null for a +// bare `typeof value` (polymorphic dispatch, not shape validation) or a +// chain rooted at any other identifier. Dynamic segments collapse to `*` +// so the count never over-relies on a computed key it cannot prove. +const getTypeofMemberCheckKey = ( + typeofUnary: EsTreeNodeOfType<"UnaryExpression">, + validatedParameterName: string, +): string | null => { + let current = stripParenExpression(typeofUnary.argument); + if (!isNodeOfType(current, "MemberExpression")) return null; + + const propertyPathSegments: string[] = []; + while (isNodeOfType(current, "MemberExpression")) { + propertyPathSegments.unshift(getStaticMemberPropertyName(current) ?? "*"); + current = stripParenExpression(current.object); + } + + if (!isNodeOfType(current, "Identifier") || current.name !== validatedParameterName) return null; + return `${current.name}.${propertyPathSegments.join(".")}`; +}; + +const getTypeofMemberCheckKeyFromComparison = ( + comparison: EsTreeNodeOfType<"BinaryExpression">, + validatedParameterName: string, +): string | null => { + if (isTypeofOperand(comparison.left) && isTypeofResultLiteral(comparison.right)) { + return getTypeofMemberCheckKey(comparison.left, validatedParameterName); + } + if (isTypeofOperand(comparison.right) && isTypeofResultLiteral(comparison.left)) { + return getTypeofMemberCheckKey(comparison.right, validatedParameterName); + } + return null; +}; + +// Collects the distinct properties of `validatedParameterName` checked +// with `typeof … === ""` inside the function body. Nested functions +// are pruned: their checks belong to their own validation, not this one. +const collectTypeofMemberCheckKeys = ( + functionNode: FunctionLikeNode, + validatedParameterName: string, +): Set => { + const checkedPropertyKeys = new Set(); + walkAst(functionNode.body, (node) => { + if (node !== functionNode.body && isFunctionLike(node)) return false; + if (!isNodeOfType(node, "BinaryExpression")) return; + if (!TYPE_COMPARISON_OPERATORS.has(node.operator)) return; + const checkKey = getTypeofMemberCheckKeyFromComparison(node, validatedParameterName); + if (checkKey) checkedPropertyKeys.add(checkKey); + }); + return checkedPropertyKeys; +}; + +const describeFunction = ( + functionName: string | null, + predicate: EsTreeNodeOfType<"TSTypePredicate"> | null, +): string => { + if (functionName) return `\`${functionName}\``; + if (predicate?.asserts) return "This assertion function"; + if (predicate) return "This type guard"; + return "This validator"; +}; + +const buildMessage = ( + functionName: string | null, + predicate: EsTreeNodeOfType<"TSTypePredicate"> | null, + validatedParameterName: string, + checkCount: number, +): string => + `${describeFunction(functionName, predicate)} hand-rolls runtime validation with ${checkCount} \`typeof\` checks on \`${validatedParameterName}\`. Parse \`${validatedParameterName}\` once with a schema validator (Zod, Valibot, Yup) to get a typed, validated value instead of maintaining the checks by hand.`; + +export const preferSchemaValidation = defineRule({ + id: "prefer-schema-validation", + title: "Hand-rolled type validation", + severity: "warn", + recommendation: + "A function that checks an object's shape with several `typeof` comparisons is a schema written by hand. Define the shape once with a schema validator (Zod, Valibot, Yup) and parse the value, so the type and the runtime check stay in sync.", + create: (context: RuleContext) => { + const inspectFunction = (functionNode: FunctionLikeNode): void => { + const predicate = getReturnTypePredicate(functionNode); + const functionName = getFunctionBindingName(functionNode); + const hasValidatorName = functionName !== null && looksLikeValidatorName(functionName); + if (!predicate && !hasValidatorName) return; + + const validatedParameterName = getValidatedParameterName(functionNode, predicate); + if (!validatedParameterName) return; + + const checkedPropertyKeys = collectTypeofMemberCheckKeys( + functionNode, + validatedParameterName, + ); + if (checkedPropertyKeys.size < MINIMUM_TYPEOF_MEMBER_CHECKS) return; + + context.report({ + node: functionNode, + message: buildMessage( + functionName, + predicate, + validatedParameterName, + checkedPropertyKeys.size, + ), + }); + }; + + return { + ArrowFunctionExpression: inspectFunction, + FunctionExpression: inspectFunction, + FunctionDeclaration: inspectFunction, + }; + }, +}); From 1df103d93017365af7953ee6430a7e900d55c5ef Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 8 Jun 2026 05:43:08 +0000 Subject: [PATCH 2/2] chore: add changeset for prefer-schema-validation rule Co-authored-by: Nisarg Patel --- .changeset/prefer-schema-validation-rule.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .changeset/prefer-schema-validation-rule.md diff --git a/.changeset/prefer-schema-validation-rule.md b/.changeset/prefer-schema-validation-rule.md new file mode 100644 index 000000000..55d488f31 --- /dev/null +++ b/.changeset/prefer-schema-validation-rule.md @@ -0,0 +1,7 @@ +--- +"oxlint-plugin-react-doctor": patch +--- + +Add the `react-doctor/prefer-schema-validation` rule. + +Flags hand-rolled runtime type/shape validation — a TypeScript type-predicate or assertion function (`value is User`, `asserts input is Config`), or a validator-named function (`isUser`, `validateConfig`, `assertX`, …) that checks an object's shape with two or more distinct `typeof` member checks — and recommends parsing the value once with a schema validator (Zod, Valibot, Yup) so the type and the runtime check stay in sync. Only `typeof param.member === ""` checks count, so polymorphic dispatch on the parameter itself, serializers without a validator name, checks inside nested functions, and dynamic computed members stay quiet.