πͺΆ Delightfully simple TypeScript validation
Because a validation library should check your data, not your patience!
β Pukka | Power |
---|---|
πͺ Simple | Write schemas as plain objects |
π― Type Safe | Full type inference |
π§ Minimal | Just 5 functions, that's it! |
π§ Custom Validation | Validations done right, your way |
π Web Standards | FormData and URLSearchParams supported out of the box |
π οΈ HTML Form Helper | Works great with Remix, react-router and Hono apps |
π Smart Types | Automatic coercion of strings to numbers, booleans and more |
π i18n Ready | Strongly typed message keys for localization |
π‘οΈ Reliable | 100% code coverage, see index.test.ts |
πͺΆ Tiny | Just 2kb minified, zero dependencies |
npm install pukka
Define your schema as a plain object, and add validations
import { object, validator } from 'pukka'
// define your schema
const schema = object({
name: "string",
age: "number",
"email?": "string", // optional field
"username:uname": "string", // field with alias
hobbies: ["string"], // array
address: { // nested object
street: "string",
city: "string",
state: "string"
}
})
// create validator
const validate = validator.for(schema, (data, issues) => {
if (data.age < 18) {
issues.age.push("Must be 18 or older");
}
});
// validate
const { success, data, errors } = validate({
name: "John",
age: "25", // will be coerced to number
uname: "johndoe", // using alias
hobbies: ["reading", "coding"],
address: {
street: "123 Main St",
city: "San Francisco",
state: "CA"
}
})
if (success) {
// strongly typed data
console.log(data.name) // string
console.log(data.age) // number
console.log(data.address.street)
} else {
console.log(errors)
console.log(data.address?.street) // street address, if entered
}
Declare and pass some runtime context to the validator
type MyContext = {
states: {
code: string
cities: string[]
}[]
}
const validate = validator.for(
schema,
usingContext<MyContext>(), // π validator needs this context
(data, issues, ctx) => {
const state = ctx.states.find((code) => code === data.state)
if(!state?.cities.includes(data.city)) {
issues.city.push(`Invalid city ${data.city}`)
}
}
)
const states = await api.getAllStates()
const { success, data } = validate(input, { states })
Builtin support for FormData and URLSearchParams
const form = new FormData()
form.append("name", "John")
form.append("address.street", "123 Some St") // π nested object
// hobbies=reading&hobbies=coding
form.append("hobbies", "reading")
form.append("hobbies", "coding")
// π indexed array
form.append("hobbies[0]", "reading")
form.append("hobbies[1]", "coding")
const { success, data } = validate(form)
As with the qs
package, an arrayLimit can be set for arrays
This is to prevent someone from sending hobbies[999999999]=cpu-hogging
const { success, data } = validate(form, {
arrayLimit: 100 // default is 50
})
Strings can be trimmed, and empty strings can be rejected
const { success, data } = validate(form, {
string: {
trim: true,
allowEmpty: false
};
})
Easily lookup the path, errors and also the submitted value for a field
import { form } from 'pukka'
const result = validate(input)
const f = form.helper(result)
<form method="post">
<input name={f.address.street.path} value={f.address.street.value}>
<span>{f.address.street.errors[0] ?? ""}</span>
</form>
Default error messages can be customized during validation
const { errors } = validate(input, {
errorMessage: (key, err) => {
if (key === "name" && err.code === "required") {
return "Please enter your full name"
}
},
});
Strongly typed keys for localization
const validate = validator.for(schema, (data, issues) => {
issues.address.street.push(key => i18n.t(key)) // key is "address.street"
issues.hobbies[0].push(key => i18n.t(key)) // key is "hobbies"
});