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 nieve

Demo

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: unknown
  • language: '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 nothing

safeParse(input, language?)

Parameters
  • input: unknown
  • language: '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 string

format(value, options?)

Parameters
  • value: Rut, or any string parse accepts, so stored values need no cast
  • options.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 separators

formatPartial(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')) // false

compare(left, right)

Parameters
  • left: unknown
  • right: 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) // false

getVerifier(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 zero

Integrations

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>
  )
}