excel

Docs › Rules

Rules format reference (rules.json)

The pure-JSON rules shared by the browser and the server: top-level keys and defaults, column rules, types, dates, row rules, blank and totals rows, formula injection, personal data, messages, hash, JSON Schema.

Rules are pure JSON: patterns are strings and there are no functions, so the rules can be hashed, shared with the server and ported to other languages. A columns array makes them 0.2 rules; without it, or with a number, the 0.1 rules (headers, a column count) apply. Giving both headers and columns[] throws 82excel: use either headers or columns[].

{
  "$schema": "https://82excel.com/schema/rules-1.json",
  "id": "orders-upload", "version": "3",
  "columns": [
    { "key": "orderNo", "header": "주문번호", "aliases": ["주문 번호", "OrderNo"],
      "nullable": false, "unique": true, "pattern": "^[A-Z]{2}\\d{6}$",
      "message": "주문번호는 AB123456 형식입니다" },
    { "key": "kind",   "header": "구분",       "enum": ["개인", "법인"] },
    { "key": "crn",    "header": "법인등록번호", "type": "kr.crn", "required": false },
    { "key": "phone",  "header": "전화번호",    "type": "kr.phone", "phone": { "kind": "mobile" } },
    { "key": "amount", "header": "금액",        "type": "kr.amount", "min": 0 },
    { "key": "start",  "header": "시작일",      "type": "date" },
    { "key": "end",    "header": "종료일",      "type": "date" }
  ],
  "extraColumns": "warn",
  "rows": { "min": 1, "max": 10000, "blank": "skip", "totals": "warn" },
  "rowRules": [
    { "when": { "col": "kind", "eq": "법인" }, "require": ["crn"] },
    { "compare": ["start", "<=", "end"] },
    { "uniqueWith": ["start", "orderNo"] }
  ],
  "formulaInjection": "warn",
  "pii": { "detect": ["rrn", "card", "phone", "email"], "mask": true }
}

defineRules(json) validates the rules, fills defaults and returns a frozen object. Unknown keys, bad types, patterns that do not compile, contradictions such as min > max, and row rules naming unknown columns throw 82excel: …. check, readRows, validate and makeTemplate accept the JSON as is or the defineRules() result.

import { defineRules, check, validate, rulesHash } from '82excel';

const rules = defineRules(await (await fetch('/rules.json')).json()); // throws "82excel: …" on a bad rules file
const r = await check(file, { ...rules, readRows: true });            // browser: the file
const v = validate(r.rows, rules);                                     // server: the cleaned rows
await rulesHash(rules);                                                // 'sha256:…', the same as r.report.schemaHash

Top-level keys and defaults

KeyDefaultMeaning
$schema, id, version—Labels; id and version go into the report and the upload body
columns(required)Column rules (table below)
columnsOrder"any"With "strict" the found columns must be in rule order (HEADER_ORDER)
extraColumns"ignore"Columns not in the rules: ignore / warn / error (COLUMN_EXTRA)
rows.min, rows.max—Result rows (blank and ignored totals rows excluded); checked by check() and validate() alike (ROWS_TOO_FEW/ROWS_TOO_MANY)
rows.blank"skip"Blank rows in the middle: skip / warn / error / stop
rows.totals"warn"A last totals row: warn / ignore / error
rowRules[]Row rules
formulaInjection"warn"warn / error / escape / off
pii.detect["rrn"]Personal data to look for in free text: rrn, card, phone, email
pii.masktrueMask personal data in messages and reports
normalize.trim / .invisible / .fullwidthDigitstrueCleanup switches for text columns
messages—Per-code wording { "CELL_TYPE": "…" }
sheet, headerRowfirst sheet, 1As in 0.1
accept, maxSize, maxUncompressedSize, maxCells, delimiteras in 0.1File options
failOn[]Warning codes promoted to errors, by check() and validate() alike
readRows, previewRows, locale, telemetry—Runtime-only keys, left out of the hash

Column rules

KeyDefaultMeaning
key(required)Key in the result row. No $ prefix ($row is reserved), no __proto__ / constructor / prototype
headerkeyHeader text, compared ignoring spacing, case and NFKC differences
aliases[]Other headers that mean the same column
requiredtrueThe column must be in the file (COLUMN_MISSING); with false its values are null when it is absent
nullabletrueEmpty cells allowed; with false an empty cell is CELL_REQUIRED
type"text"See the types table
uniquefalseNo duplicates in the column (CELL_UNIQUE, from the second one)
enum—Allowed values, strict equality on the cleaned value (1 ≠ "1")
pattern—ECMAScript regex string compiled with the u flag, tested on cleaned string values (anchor with ^…$)
min, max—Range for numeric values (CELL_RANGE)
minLength, maxLength—, 32767String length in UTF-16 units; values over maxLength skip the pattern
date{ formats: defaults, pivot: 50 }Date formats (below)
phone.kind"any"mobile / landline / any
boolean{ true: ["Y","O","예","TRUE"], false: ["N","X","아니오","FALSE"] }Case-insensitive; a value in both lists is true
rrn.checksum"warn"off turns off the RRN_CHECKSUM warning
crn.checksum"off"warn turns on the CRN_CHECKSUM warning
severity"error"Severity of this column's rule failures: error / warning
pii—allow: no personal data detection in this column / forbid: detection is an error (PII_FORBIDDEN)
message—Wording for this column's cell issues

Each cell is checked empty → type → enum → pattern → range → length → unique. A type failure stops the rest for that cell. Pattern and length apply to string values, min/max to numbers.

Types and canonical forms

A cleaned (canonical) value always passes again, and cleaning twice gives the same value (idempotent). The server receives cleaned values.

typeAcceptsCanonicalCode
textAny string. Number cells become strings (with leading zeros under a 00000 format), booleans TRUE/FALSEstringCELL_TYPE
numberNumbers, 1,234.5, 1., 1e3, full-width digits. 0,123 and 1천 are rejectedJS numberCELL_TYPE
integerWhole numbers within ±2^53−1JS numberCELL_TYPE
dateYYYY-MM-DD, the default formats, Excel date cellsYYYY-MM-DDCELL_DATE
booleanValues of the boolean table, boolean cellstrue / falseCELL_TYPE
emaila@b.c shape (case kept)stringCELL_TYPE
kr.phone, kr.brn, kr.crn, kr.rrn, kr.zip, kr.amountFormats for Korean business data (phone numbers, business and resident registration numbers, postal codes, won amounts). See Korean validators010-1234-5678, 123-45-67890 …KR_* and others
  • Empty means null, missing, or '' after cleanup. The number 0 and false are not empty. Empty cells are null in result rows.
  • A cell failing its type keeps its raw input in the row; one that passes its type but fails a later rule (enum …) keeps its cleaned value.
  • integer and kr.amount reject values beyond ±2^53−1 (Number.MAX_SAFE_INTEGER).

Cleanup

  • Typed columns (number, integer, date, boolean, email, kr.*) always tolerate surrounding whitespace, invisible characters (zero-width, BOM, NBSP) and full-width digits and signs, whatever normalize says.
  • The normalize switches govern text output only: trim (both ends, U+3000 included), invisible (zero-width and BOM removed, NBSP → space).
  • text keeps full-width characters as typed (1호점), so fullwidthDigits currently changes nothing (accepted for compatibility). No NFKC on text.

Dates

  • Output is always YYYY-MM-DD, and that canonical form is always accepted whatever formats says. Years 0001–9999, Gregorian leap years.
  • Tokens: YYYY (4 digits), YY (2 digits; below pivot is 20YY, else 19YY; default 50), MM/DD (exactly 2 digits), M/D (1–2 digits). Literals: . - / space 년 월 일. One year, month and day token each.
  • Default formats: YYYY-M-D, YYYY.M.D, YYYY. M. D., YYYY.M.D., YYYY/M/D, YY-M-D, YYYYMMDD, YYYY년 M월 D일, YYYY년M월D일.
  • Excel date cells. Date-formatted number cells are Excel serials (from 1899-12-30, the 1900-02-29 bug handled, date1904 honoured); the time is dropped. This happens when the file is read, so a server never sees a serial.
  • A number without a date format (and any JSON number) is read as its digits: 20240105 → 2024-01-05.

Column mapping

  • header and aliases are matched against the header row ignoring spacing, case and NFKC differences. Result rows are keyed by key, not by the header text.
  • A missing required: true column is COLUMN_MISSING (error); a column found twice is COLUMN_DUPLICATE (the first is used); columns not in the rules follow extraColumns (COLUMN_EXTRA).
  • The columns of a check() result is [{ key, source, col }] (the header text found and the column letter).

Row rules

RuleMeaningCode (column it is on)
{ "when": { "col": "kind", "eq": "법인" }, "require": ["crn"] }Required when the condition holdsROW_REQUIRE (the required column)
{ "when": { … }, "forbid": ["x"] }Must be empty when the condition holdsROW_FORBID (the forbidden column)
{ "compare": ["start", "<=", "end"] }Compare two columns < <= = != >= > (dates, numbers)ROW_COMPARE (the left column)
{ "uniqueWith": ["date", "orderNo"] }Unique combination of columnsROW_UNIQUE_WITH (the first column)
  • when takes one of eq, neq, in, empty: true, notEmpty: true, with strict equality on cleaned values; an empty cell only matches empty.
  • compare is skipped when either cell is empty or failed its type; uniqueWith skips rows with an empty cell in the set.
  • Each rule may carry severity and message. Columns named by a rule must exist in columns (catches typos).
  • JS hooks: validate(rows, rules, { rowRules: [(row) => [...issues]] }). They are not part of the JSON, so the server must implement the same hook itself.

Blank and totals rows

  • rows.blank: skip (default, left out), warn / error (BLANK_ROW), stop (stop at the first blank row; later rows are not read). Under any policy a blank row is never a result row, and blank rows after the last data row are dropped silently.
  • rows.totals: the last data row is a totals row when its first value starts with 합계, 소계, 총계 or Total, or when every cell is a formula. warn (default) / error (TOTALS_ROW) / ignore (left out). It needs the file, so only the browser (check, readRows) checks it.

Formula injection

  • Strings in text and email columns that start with = + - @, a tab or CR. Real formula cells in text columns of an xlsx count too (browser only).
  • No false alarms: values that read as numbers, phone numbers or amounts (-1000, +82 10-1234-5678, -₩5,000) and values already starting with ' are left alone.
  • warn (default) / error / escape (a ' is put in front of the result value; '=… is canonical, so it passes on the server too, and unique / row rules compare without the ') / off.
  • Values containing HYPERLINK(, DDE(, cmd|, IMPORTXML( or IMPORTDATA( are errors under every setting, off included.

Personal data

  • Detection (pii.detect, text columns only): rrn (Korean resident registration numbers: format and birth date, no checksum, so numbers issued since 2020 are caught too), card (13–19 digits, Luhn), phone (Korean mobile numbers), email. Default ["rrn"]. One PERSONAL_DATA warning per column, at the first row, with the count and the rows.
  • Per column: pii: "allow" turns detection off there; pii: "forbid" makes a hit the error PII_FORBIDDEN. Columns whose type is meant to hold that data (such as kr.rrn) are not scanned.
  • Masking (pii.mask: true by default): RRN 900101-1******, card 1234-****-****-5678, phone 010-****-5678, email a***@b.com, in messages, the raw/value of issues, summary examples, result.report, the CSV and xlsx error reports and the component's previews.
  • result.rows is never masked: it is the data you upload.
  • Detection takes time linear in the cell length; values over maxLength are already CELL_LENGTH and are not scanned.

Messages

Cell codes are fixed upper-case constants (Error codes), English by default and Korean with locale: 'ko'. A column message applies to that column's cell issues; messages: { CELL_TYPE: '…' } (in the rules or as an option) applies to every 0.2 code and is overridden by a column message. {row}, {col}, {header}, {raw}, {min} … are filled in.

Hash

rulesHash(rules) is the SHA-256 of the canonical JSON (sorted keys, no whitespace), as sha256:<hex>. For a defineRules() result it hashes the JSON as given, before defaults. readRows, previewRows, locale and telemetry are left out, so the browser report's schemaHash equals what the server computes from its own rules.json.

JSON Schema and editor completion

The JSON Schema (draft 2020-12) of the rules file is served at https://82excel.com/schema/rules-1.json (application/json, CORS open). Put it in $schema and editors such as VS Code complete keys and values and flag unknown keys. The npm package ships the same file as 82excel/rules-1.schema.json.

{
  "$schema": "https://82excel.com/schema/rules-1.json",
  "columns": [ … ]
}