Notion Valibot Schema
Turn Notion's nested API responses into clean, typed JavaScript values.
This library provides a collection of Valibot schemas specifically designed to handle Notion API objects. It doesn't just validate; it transforms deeply nested Notion properties into simple, usable primitives like string, number, Date, and boolean.
The Problem
When you fetch a page from Notion, properties are deeply nested. To access them type-safely, you end up writing verbose type guards for every single property.
// 😫 The "Native" Way (Boilerplate Hell)
// 1. Get the property
const statusProp = page.properties["Status"];
// 2. Check if it exists and has the correct type
if (statusProp?.type === "status" && statusProp.status) {
// 3. Finally access the value
console.log(statusProp.status.name); // "In Progress"
}
// Repeat this for every property...
const tagsProp = page.properties["Tags"];
if (tagsProp?.type === "multi_select") {
console.log(tagsProp.multi_select.map(t => t.name));
}The Solution
With @nakanoaas/notion-valibot-schema, you get this:
// After parsing
{
Status: "In Progress",
Tags: ["Urgent", "Work"],
DueDate: new Date("2023-12-25"),
Assignee: ["user-id-1", "user-id-2"]
}No more checking for property.type === 'date', handling null, or digging through 3 layers of objects just to get a string.
Features
- Composable: Works seamlessly with standard Valibot schemas (
v.object,v.array, etc.). - Transformative: Automatically extracts values (e.g.,
RichText[]->string). - Type-Safe: Full TypeScript support with inferred types.
- Well Tested: Backed by a comprehensive test suite covering edge cases.
- Comprehensive: Supports complex properties like Rollups, Formulas, and Relations.
Installation
Node.js (npm / pnpm / yarn / bun)
npm install @nakanoaas/notion-valibot-schema valibotpnpm add @nakanoaas/notion-valibot-schema valibotDeno / JSR
deno add @nakanoaas/notion-valibot-schema @valibot/valibotUsage
Basic Example
Here is how to validate and transform a Notion page retrieved from the API.
import * as v from "valibot";
import {
TitleSchema,
RichTextSchema,
StatusSchema,
MultiSelectSchema,
NullableSingleDateSchema,
CheckboxSchema,
PeopleIdSchema,
} from "@nakanoaas/notion-valibot-schema";
// 1. Define your schema based on your Data Source properties
const TaskPageSchema = v.object({
id: v.string(),
properties: v.object({
// Map "Name" property -> string
Name: TitleSchema,
// Map "Description" property -> string
Description: RichTextSchema,
// Map "Status" property -> "ToDo" | "Doing" | "Done"
Status: StatusSchema(v.picklist(["ToDo", "Doing", "Done"])),
// Map "Tags" -> string[]
Tags: MultiSelectSchema(v.string()),
// Map "Due Date" -> Date | null
DueDate: NullableSingleDateSchema,
// Map "IsUrgent" -> boolean
IsUrgent: CheckboxSchema,
// Map "Assignee" -> string[] (User IDs)
Assignee: PeopleIdSchema,
}),
});
// 2. Fetch data from Notion
const page = await notion.pages.retrieve({ page_id: "..." });
// 3. Parse and transform
const task = v.parse(TaskPageSchema, page);
// 4. Use your clean data
console.log(task.properties.Name); // "Buy Milk" (string)
console.log(task.properties.DueDate); // Date object or null
console.log(task.properties.Tags); // ["Personal", "Shopping"] (string[])
console.log(task.properties.Assignee); // ["user-id-1", "user-id-2"] (string[])Handling Lists (Query Results)
To parse the results of a data source query:
const TaskListSchema = v.array(TaskPageSchema);
const { results } = await notion.dataSources.query({ data_source_id: "..." });
const tasks = v.parse(TaskListSchema, results);Schema Reference
For complete API documentation, including all available schemas and types, please visit the JSR Documentation.
| Notion Property | Schema | Transformed Output (Type) |
|---|---|---|
| Text / Title | TitleSchema / RichTextSchema / NullableTitleSchema / NullableRichTextSchema |
string / string | null |
| Number | NumberSchema / NullableNumberSchema |
number / number | null |
| Checkbox | CheckboxSchema |
boolean |
| Select | SelectSchema(schema) |
Inferred<schema> |
| Multi-Select | MultiSelectSchema(schema) |
Inferred<schema>[] |
| Status | StatusSchema(schema) |
Inferred<schema> |
| Date (Single) | SingleDateSchema / NullableSingleDateSchema |
Date / Date | null |
| Date (Range) | RangeDateSchema / NullableRangeDateSchema |
{ start: Date; end: Date; time_zone: string | null } / { start: Date; end: Date; time_zone: string | null } | null |
| Date (Full) | FullDateSchema / NullableFullDateSchema |
{ start: Date; end: Date | null; time_zone: string | null } / { start: Date; end: Date | null; time_zone: string | null } | null |
| Relation | RelationSchema |
string[] (Page IDs) |
| Relation (Single) | SingleRelationSchema |
string (Page ID) |
| Rollup (Simple) | RollupSimpleSchema(schema) |
Inferred<schema> |
| Rollup (Array) | RollupArraySchema(schema) |
Inferred<schema>[] |
| Rollup (Single) | SingleRollupArraySchema(schema) |
Inferred<schema> |
| Rollup (Single, Nullable) | NullableSingleRollupArraySchema(schema) |
Inferred<schema> | null |
| Formula | FormulaSchema(schema) |
Inferred<schema> |
| URL | UrlSchema |
string |
EmailSchema |
string |
|
| Phone | PhoneNumberSchema |
string |
| Files | FileSchema |
string[] (URLs) |
| Files (Single) | SingleFileSchema / NullableSingleFileSchema |
string (URL) / string | null |
| People (building blocks) | UserOrGroupIdSchema / UserOrGroupSchema / UserSchema / PersonSchema / BotSchema |
Building-block object types |
| People (generic) | PeopleSchema(schema) / SinglePeopleSchema(schema) / NullableSinglePeopleSchema(schema) |
Inferred<schema>[] / Inferred<schema> / Inferred<schema> | null |
| People (convenience) | PeopleIdSchema / SinglePeopleIdSchema / NullableSinglePeopleIdSchema |
string[] / string / string | null |
| Created/Edited By (generic) | CreatedBySchema(schema) / LastEditedBySchema(schema) |
Inferred<schema> |
| Created/Edited By (convenience) | CreatedByIdSchema / NullableCreatedByNameSchema / LastEditedByIdSchema / NullableLastEditedByNameSchema |
string / string | null |
| Created/Edited Time | CreatedTimeSchema / LastEditedTimeSchema |
Date |
| Place | PlaceSchema / NullablePlaceSchema |
{ lat: number; lon: number; name?: string | null; address?: string | null } / { lat: number; lon: number; name?: string | null; address?: string | null } | null |
| Unique ID | UniqueIdNumberSchema / PrefixedUniqueIdStringSchema / NullableUniqueIdSchema |
number / string (e.g. "PREFIX-123") / { prefix: string | null; number: number | null } |
| Verification | VerificationSchema / NullableVerificationSchema |
{ state: "unverified" | "verified" | "expired"; date: DateObject | null; verified_by: { id: string; object: "user"; name: string | null; avatar_url: string | null } | null } / same | null |
Advanced Schemas
Formulas
Formulas in Notion can return different types (string, number, boolean, date). Use FormulaSchema with the matching inner schema for each formula property.
import * as v from "valibot";
import {
BooleanFormulaSchema,
FormulaSchema,
NumberSchema,
SingleDateSchema,
StringFormulaSchema,
} from "@nakanoaas/notion-valibot-schema";
const PageSchema = v.object({
id: v.string(),
properties: v.object({
FormulaText: FormulaSchema(StringFormulaSchema),
FormulaNumber: FormulaSchema(NumberSchema),
FormulaBoolean: FormulaSchema(BooleanFormulaSchema),
FormulaDate: FormulaSchema(SingleDateSchema),
}),
});
const page = await notion.pages.retrieve({ page_id: "..." });
const parsed = v.parse(PageSchema, page);
// parsed.properties.FormulaText: string
// parsed.properties.FormulaNumber: number
// parsed.properties.FormulaBoolean: boolean
// parsed.properties.FormulaDate: DateRollups
Rollups are powerful but complex. We provide helpers for common rollup types.
import {
RollupSimpleSchema,
RollupArraySchema,
SingleRollupArraySchema,
NullableSingleRollupArraySchema,
NumberSchema,
SingleDateSchema,
} from "@nakanoaas/notion-valibot-schema";
const MySchema = v.object({
// Sum/Average rollup (returns number)
TotalCost: RollupSimpleSchema(NumberSchema),
// Date rollup (returns Date)
LatestMeeting: RollupSimpleSchema(SingleDateSchema),
// Array rollup (e.g., pulling tags from related items)
AllTags: RollupArraySchema(v.string()),
// Single-element array rollup (returns the one item, not an array)
PrimaryTag: SingleRollupArraySchema(v.string()),
// Single-element or empty array rollup (returns item or null)
OptionalPrimaryTag: NullableSingleRollupArraySchema(v.string()),
});People & Created/Edited By
People-related schemas are organized into building blocks, generic factories, and convenience schemas:
- Building blocks (
UserOrGroupIdSchema,UserOrGroupSchema,UserSchema,PersonSchema,BotSchema) validate individual user/group objects. - Generic factories (
PeopleSchema,SinglePeopleSchema,NullableSinglePeopleSchema,CreatedBySchema,LastEditedBySchema) accept a building-block schema and extract the property value. - Convenience schemas (
PeopleIdSchema,CreatedByIdSchema, etc.) require no generic parameter and extract common primitives like IDs or names.
import * as v from "valibot";
import {
CreatedByIdSchema,
CreatedBySchema,
PeopleIdSchema,
PeopleSchema,
PersonSchema,
UserOrGroupSchema,
UserSchema,
} from "@nakanoaas/notion-valibot-schema";
const PageSchema = v.object({
id: v.string(),
properties: v.object({
// Full person details
People: PeopleSchema(PersonSchema),
// IDs only (no generic needed)
AssigneeIds: PeopleIdSchema,
// created_by ID extraction
CreatedById: CreatedByIdSchema,
// Custom user schema
CreatedBy: CreatedBySchema(UserSchema),
}),
});Migration Guide (Breaking Changes)
The following schemas changed from constants to generic factories:
| Before | After |
|---|---|
PeopleSchema |
PeopleSchema(UserOrGroupSchema) or PeopleSchema(PersonSchema) |
CreatedBySchema |
CreatedBySchema(UserOrGroupSchema) |
LastEditedBySchema |
LastEditedBySchema(UserOrGroupSchema) |
For ID-only extraction, use the convenience schemas instead: PeopleIdSchema, CreatedByIdSchema, LastEditedByIdSchema.