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.schemaHashTop-level keys and defaults
| Key | Default | Meaning |
|---|---|---|
$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.mask | true | Mask personal data in messages and reports |
normalize.trim / .invisible / .fullwidthDigits | true | Cleanup switches for text columns |
messages | — | Per-code wording { "CELL_TYPE": "…" } |
sheet, headerRow | first sheet, 1 | As in 0.1 |
accept, maxSize, maxUncompressedSize, maxCells, delimiter | as in 0.1 | File 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
| Key | Default | Meaning |
|---|---|---|
key | (required) | Key in the result row. No $ prefix ($row is reserved), no __proto__ / constructor / prototype |
header | key | Header text, compared ignoring spacing, case and NFKC differences |
aliases | [] | Other headers that mean the same column |
required | true | The column must be in the file (COLUMN_MISSING); with false its values are null when it is absent |
nullable | true | Empty cells allowed; with false an empty cell is CELL_REQUIRED |
type | "text" | See the types table |
unique | false | No 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 | —, 32767 | String 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.
| type | Accepts | Canonical | Code |
|---|---|---|---|
text | Any string. Number cells become strings (with leading zeros under a 00000 format), booleans TRUE/FALSE | string | CELL_TYPE |
number | Numbers, 1,234.5, 1., 1e3, full-width digits. 0,123 and 1천 are rejected | JS number | CELL_TYPE |
integer | Whole numbers within ±2^53−1 | JS number | CELL_TYPE |
date | YYYY-MM-DD, the default formats, Excel date cells | YYYY-MM-DD | CELL_DATE |
boolean | Values of the boolean table, boolean cells | true / false | CELL_TYPE |
email | a@b.c shape (case kept) | string | CELL_TYPE |
kr.phone, kr.brn, kr.crn, kr.rrn, kr.zip, kr.amount | Formats for Korean business data (phone numbers, business and resident registration numbers, postal codes, won amounts). See Korean validators | 010-1234-5678, 123-45-67890 … | KR_* and others |
- Empty means
null, missing, or''after cleanup. The number0andfalseare not empty. Empty cells arenullin 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.
integerandkr.amountreject 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, whatevernormalizesays. - The
normalizeswitches governtextoutput only:trim(both ends, U+3000 included),invisible(zero-width and BOM removed, NBSP → space). textkeeps full-width characters as typed (1호점), sofullwidthDigitscurrently changes nothing (accepted for compatibility). No NFKC on text.
Dates
- Output is always
YYYY-MM-DD, and that canonical form is always accepted whateverformatssays. Years 0001–9999, Gregorian leap years. - Tokens:
YYYY(4 digits),YY(2 digits; belowpivotis 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,
date1904honoured); 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
headerandaliasesare matched against the header row ignoring spacing, case and NFKC differences. Result rows are keyed bykey, not by the header text.- A missing
required: truecolumn isCOLUMN_MISSING(error); a column found twice isCOLUMN_DUPLICATE(the first is used); columns not in the rules followextraColumns(COLUMN_EXTRA). - The
columnsof acheck()result is[{ key, source, col }](the header text found and the column letter).
Row rules
| Rule | Meaning | Code (column it is on) |
|---|---|---|
{ "when": { "col": "kind", "eq": "법인" }, "require": ["crn"] } | Required when the condition holds | ROW_REQUIRE (the required column) |
{ "when": { … }, "forbid": ["x"] } | Must be empty when the condition holds | ROW_FORBID (the forbidden column) |
{ "compare": ["start", "<=", "end"] } | Compare two columns < <= = != >= > (dates, numbers) | ROW_COMPARE (the left column) |
{ "uniqueWith": ["date", "orderNo"] } | Unique combination of columns | ROW_UNIQUE_WITH (the first column) |
whentakes one ofeq,neq,in,empty: true,notEmpty: true, with strict equality on cleaned values; an empty cell only matchesempty.compareis skipped when either cell is empty or failed its type;uniqueWithskips rows with an empty cell in the set.- Each rule may carry
severityandmessage. Columns named by a rule must exist incolumns(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합계,소계,총계orTotal, 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
textandemailcolumns 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(orIMPORTDATA(are errors under every setting,offincluded.
Personal data
- Detection (
pii.detect,textcolumns 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"]. OnePERSONAL_DATAwarning 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 errorPII_FORBIDDEN. Columns whose type is meant to hold that data (such askr.rrn) are not scanned. - Masking (
pii.mask: trueby default): RRN900101-1******, card1234-****-****-5678, phone010-****-5678, emaila***@b.com, in messages, theraw/valueof issues,summaryexamples,result.report, the CSV and xlsx error reports and the component's previews. result.rowsis never masked: it is the data you upload.- Detection takes time linear in the cell length; values over
maxLengthare alreadyCELL_LENGTHand 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": [ … ]
}