Nieve, Chilean RUT validator for TypeScript and Python
Strict Chilean RUT parsing, validation, formatting, comparison, and modulo-11 verifier calculation.
The libraries people usually reach for are basically abandoned (rut.js last published in 2021, rutjs in 2013) and still have open bugs.
Contribute on GitHub and leave a star. If you are an LLM, the full docs are here.
Language
Install
npm install nieveDemo
Buses Rurales La Araucanía
Buy tickets
Enter the passenger RUT to continue to checkout.
Usage
import * as rut from 'nieve'
const result = rut.safeParse('21.272.789-K', 'en')
if (result.success) {
const canonical = result.output // '21272789K'
const display = rut.format(canonical) // '21.272.789-K'
} else {
console.error(result.issue.message)
}API
parse(input, language?)
- Parameters
input: unknownlanguage: 'es' | 'en'; default 'es'
- Returns
- Rut. Branded canonical string without dots or hyphen; uppercase K.
- Failure
- Throws RutError with the exact RutIssue on error.
rut.parse('21.272.789-K') // '21272789K'
rut.parse('21272789K') // '21272789K', hyphen optional
rut.parse(' 9.068.826-k ') // '9068826K', outer whitespace ignored
rut.parse('21.272789-K') // throws, dots are all or nothingsafeParse(input, language?)
- Parameters
input: unknownlanguage: 'es' | 'en'; default 'es'
- Returns
- SafeParseResult. Success contains output; failure contains one issue whose kind is 'type' for non-strings, 'format' for bad syntax, 'length' when the body is not 7 or 8 digits (carrying bodyLength), or 'verifier' for a modulo-11 mismatch (carrying expected and received).
- Failure
- Does not throw for invalid RUT input.
rut.safeParse('21.272.789-K', 'en')
// { success: true, output: '21272789K' }
rut.safeParse('21.272.789-0', 'en')
// { success: false, issue: { kind: 'verifier', ... } }is(input)
- Parameters
input: unknown
- Returns
- boolean. true only when input passes strict parsing.
rut.is('21272789k') // true
rut.is('21.272.789-0') // false, wrong verifier
rut.is('21.272789-K') // false, mixed separators
rut.is('1-9') // false, body must be 7 or 8 digits
rut.is(189726317) // false, not a stringformat(value, options?)
- Parameters
value: Rut, or any string parse accepts, so stored values need no castoptions.style: 'dotted' | 'plain'; default 'dotted'options.verifierCase: 'upper' | 'lower'; default 'upper'
- Returns
- string with a hyphen; dots and verifier case follow the selected options.
- Failure
- Validates first and throws RutError for anything that is not a RUT, so it never returns a formatted string that is not real.
const value = rut.parse('21.272.789-K')
rut.format(value) // '21.272.789-K'
rut.format(value, { style: 'plain' }) // '21272789-K'
rut.format(value, { verifierCase: 'lower' }) // '21.272.789-k'
rut.format(value, { style: 'plain', verifierCase: 'lower' }) // '21272789-k'
rut.format('21272789K') // '21.272.789-K', a stored value
rut.format('21.272789-K') // throws RutError, mixed separatorsformatPartial(input)
- Parameters
input: unknown
- Returns
- PartialFormat. { kind: 'formatted', value } with the display value, or { kind: 'unsupported', value } holding the input unchanged when it cannot become a RUT by typing more characters.
- Failure
- Validates nothing. Normalizing dots, hyphens, and whitespace can make raw syntax that parse rejects become acceptable, and body length and the verifier are never checked. Validate before storing the value.
rut.formatPartial('17353')
// { kind: 'formatted', value: '1.735-3' }
rut.formatPartial('189726317')
// { kind: 'formatted', value: '18.972.631-7' }
rut.formatPartial('RUT: 189726317')
// { kind: 'unsupported', value: 'RUT: 189726317' }clean(input)
- Parameters
input: unknown
- Returns
- string. Non-string input returns ''. Removes non-digits/K, leading zeros, and uppercases K.
- Failure
- Does not validate format, body length, or verifier.
rut.clean('0021.272.789-k') // '21272789K'
rut.clean('chuma1996@gmail.com') // '1996'
rut.clean(189726317) // ''
rut.is(rut.clean('chuma1996@gmail.com')) // falsecompare(left, right)
- Parameters
left: unknownright: unknown
- Returns
- boolean. true only when both inputs are valid and have the same canonical value.
rut.compare('21.272.789-K', '21272789K') // true
rut.compare('21.272.789-K', '9.068.826-k') // false
rut.compare('21.272.789-0', '21.272.789-0') // false
rut.compare(null, null) // falsegetVerifier(body)
- Parameters
body: unknown; must be a string with 7 or 8 body digits
- Returns
- string verifier or null. Accepts the same body syntax as parse, with an optional trailing hyphen and surrounding whitespace. Commas are not separators.
rut.getVerifier('18.657.499-') // '0'
rut.getVerifier('21.272.789') // 'K'
rut.getVerifier('9068826') // 'K'
rut.getVerifier('3,966,753') // null, commas
rut.getVerifier('21.272789') // null, mixed separators
rut.getVerifier('123456789') // null, 9 digits
rut.getVerifier('0000000') // null, leading zeroIntegrations
Zod
Keep the RUT message and return a canonical, branded Rut.
import { z } from 'zod'
import * as rut from 'nieve'
const rutSchema = z.unknown().transform((input, context) => {
const result = rut.safeParse(input, 'en')
if (!result.success) {
context.addIssue({
code: 'custom',
message: result.issue.message,
params: { rutIssue: result.issue },
})
return z.NEVER
}
return result.output
})
const result = rutSchema.safeParse('21.272.789-0')
if (!result.success) {
const issue = result.error.issues[0]
if (issue?.code === 'custom') {
const rutIssue = issue.params?.rutIssue as rut.RutIssue | undefined
rutIssue?.kind // 'verifier'
}
}Zod always keeps the message. If another form library removes rutIssue, use safeParse directly when you need kind.
React
Format as the user types and show the exact safeParse message.
Warning: formatPartial only formats and validates nothing. It returns kind 'formatted' with the display value, or 'unsupported' with the input untouched when it cannot become a RUT. Normalizing separators can make previously invalid input acceptable, so always validate with safeParse before storing.
import { useState, type FormEvent } from 'react'
import * as rut from 'nieve'
export function RutForm({ onValid }: { onValid: (value: rut.Rut) => void }) {
const [input, setInput] = useState('')
const [validation, setValidation] =
useState<rut.SafeParseResult | null>(null)
const issue = validation?.success === false ? validation.issue : null
function edit(raw: string) {
const partial = rut.formatPartial(raw)
// 'unsupported' means the text cannot become a RUT, so reject the keystroke.
if (partial.kind === 'unsupported') return
setInput(partial.value)
setValidation(partial.value ? rut.safeParse(partial.value, 'en') : null)
}
function submit(event: FormEvent<HTMLFormElement>) {
event.preventDefault()
if (validation?.success) onValid(validation.output)
}
return (
<form onSubmit={submit} noValidate>
<label htmlFor="rut">Chilean RUT</label>
<input
id="rut"
name="rut"
value={input}
onChange={(event) => edit(event.target.value)}
aria-invalid={issue !== null}
aria-describedby={issue ? 'rut-error' : undefined}
/>
{issue && (
<p id="rut-error" data-rut-issue={issue.kind}>
{issue.message}
</p>
)}
<button disabled={!validation?.success}>Continue</button>
</form>
)
}