Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
fix: handle OAS 3.1 type arrays and const schema values
type: ["string", "null"] (valid in OAS 3.1) fell through to v.unknown()
because neither the type nor valibot codegen handled array-form types.
const schemas were ignored in valibot output entirely, and only handled
for strings in type generation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  • Loading branch information
maxholman and claude committed Apr 2, 2026
commit 064751e38e4b5e652ac58ccf4a908172169c91ca
64 changes: 44 additions & 20 deletions lib/process-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -568,6 +568,50 @@ export function registerTypesFromSchema(
typesAndInterfaces.set(`#/components/schemas/${schemaName}`, typeAlias);
}

// deal with type arrays (OpenAPI 3.1: type: ["string", "null"])
else if (Array.isArray(schemaObject.type)) {
const prop = schemaToType(
typesAndInterfaces,
{},
schemaName,
schemaObject,
);

const typeAlias = typesFile.addTypeAlias({
name: pascalCase(schemaName),
isExported: true,
type: prop.type || "unknown",
});

if (schemaObject.description) {
typeAlias.addJsDoc({
description: wordWrap(schemaObject.description),
});
}

typesAndInterfaces.set(`#/components/schemas/${schemaName}`, typeAlias);
}

// deal with const values
else if ("const" in schemaObject) {
const constDeclaration = typesFile.addTypeAlias({
isExported: true,
name: pascalCase(schemaName),
type: JSON.stringify(schemaObject.const),
});

if (schemaObject.description) {
constDeclaration.addJsDoc({
description: wordWrap(schemaObject.description),
});
}

typesAndInterfaces.set(
`#/components/schemas/${schemaName}`,
constDeclaration,
);
}

// deal with objects
else if (!schemaObject.type || schemaObject.type === "object") {
const newIf = typesFile.addTypeAlias({
Expand Down Expand Up @@ -643,26 +687,6 @@ export function registerTypesFromSchema(
// );
}

// deal with string consts
else if (schemaObject.type === "string" && "const" in schemaObject) {
const constDeclaration = typesFile.addTypeAlias({
isExported: true,
name: pascalCase(schemaName),
type: JSON.stringify(schemaObject.const),
});

if (schemaObject.description) {
constDeclaration.addJsDoc({
description: wordWrap(schemaObject.description),
});
}

typesAndInterfaces.set(
`#/components/schemas/${schemaName}`,
constDeclaration,
);
}

// deal with non-enum strings
else if (schemaObject.type === "string" && !schemaObject.enum) {
const typeAlias = typesFile.addTypeAlias({
Expand Down
37 changes: 37 additions & 0 deletions lib/valibot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,43 @@ export function schemaToValidator(
? `v.custom<${typescriptHint}>(() => true)`
: undefined;

// Handle const values (OpenAPI 3.1: const: "value")
if ("const" in schema) {
return schema.const === null
? vcall("null")
: maybeNullable(
vcall("literal", JSON.stringify(schema.const)),
isNullable,
);
}

// Handle type arrays (OpenAPI 3.1: type: ["string", "null"])
if (Array.isArray(schema.type)) {
const nonNullTypes = schema.type.filter((t) => t !== "null");
const [singleType] = nonNullTypes;

if (nonNullTypes.length === 1 && singleType) {
return maybeNullable(
schemaToValidator(validators, {
...schema,
type: singleType,
} satisfies typeof schema),
isNullable,
);
}

const variants = nonNullTypes.map((t) =>
schemaToValidator(validators, {
...schema,
type: t,
} satisfies typeof schema),
);
return maybeNullable(
variants.length > 0 ? vcall("union", variants) : vcall("unknown"),
isNullable,
);
}

if (schema.type === "string") {
if (schema.enum) {
return maybeNullable(
Expand Down