@alex-roc/xlsform2json
v0.1.0
Published
Parse XLSForm Excel files and convert them to a JSON structure consumable by React Hook Form + Yup
Maintainers
Readme
xlsform2json
Parse XLSForm Excel files (.xlsx) and convert them to a JSON structure ready for use with React Hook Form + Yup.
npm install @alex-roc/xlsform2jsonQuick start
import { convertXLSForm } from '@alex-roc/xlsform2json'
const form = await convertXLSForm('./my-form.xlsx')
// or pass a Buffer / ArrayBuffer for browser / server useOutput:
{
"version": "1",
"fields": [
{
"type": "integer",
"name": "patient_age",
"label": "Patient age",
"hint": "In full years",
"visibleWhen": null,
"validation": {
"required": true,
"constraint": ". >= 0 and . <= 150",
"constraintMessage": "Age must be 0-150"
}
},
{
"type": "select_one",
"name": "gender",
"label": "Gender",
"hint": null,
"listName": "gender",
"visibleWhen": null,
"validation": { "required": true, "constraint": null, "constraintMessage": null }
}
],
"choices": {
"gender": [
{ "value": "male", "label": "Male" },
{ "value": "female", "label": "Female" }
]
}
}API
convertXLSForm(source)
import { convertXLSForm } from '@alex-roc/xlsform2json'
async function convertXLSForm(
source: string | Buffer | ArrayBuffer
): Promise<XLSFormJSON>| Argument | Description |
|---|---|
| string | Absolute or relative file path (Node.js only) |
| Buffer | Node.js Buffer of the .xlsx file |
| ArrayBuffer | Browser ArrayBuffer (e.g. from FileReader or fetch) |
Throws XLSFormParseError if required worksheets or columns are missing, or if an unsupported field type is encountered.
buildYupSchema(form) — separate entry point
Requires yup >= 1.0.0 as a peer dependency.
import { convertXLSForm } from '@alex-roc/xlsform2json'
import { buildYupSchema } from '@alex-roc/xlsform2json/yup'
import { useForm } from 'react-hook-form'
import { yupResolver } from '@hookform/resolvers/yup'
const form = await convertXLSForm('./my-form.xlsx')
const schema = buildYupSchema(form)
const { register, handleSubmit } = useForm({ resolver: yupResolver(schema) })Note: XLSForm
constraintexpressions are raw XPath strings and cannot be converted to Yup rules automatically in v1. Aconsole.warnis emitted for each field that has a non-nullconstraintso you can add the validation manually.
Supported field types
| XLSForm type | JSON type | Extra fields |
|---|---|---|
| text | text | — |
| integer | integer | — |
| decimal | decimal | — |
| date | date | — |
| time | time | — |
| dateTime | dateTime | — |
| geopoint | geopoint | — |
| acknowledge | acknowledge | — |
| note | note | — |
| select_one <list> | select_one | listName |
| select_multiple <list> | select_multiple | listName |
| select_one_from_file <file> | select_one_from_file | sourceFile |
| select_multiple_from_file <file> | select_multiple_from_file | sourceFile |
| component <Name> | component | componentName, props |
Survey columns parsed
| Column | Notes |
|---|---|
| type | Required |
| name | Required. Used as the field identifier |
| label | Required. Display text |
| hint | Optional. null if empty |
| required | "yes" → true, anything else → false |
| constraint | Stored as raw string or null |
| constraint_message | Stored as raw string or null |
| relevant | Parsed into visibleWhen (see below) |
| parameters | Parsed into props for component fields |
visibleWhen — parsing the relevant column
Simple and compound expressions with and / or are parsed automatically.
Single condition:
"visibleWhen": {
"raw": "${likes_pizza} = 'yes'",
"conditions": [{ "field": "likes_pizza", "operator": "=", "value": "yes" }],
"logic": null
}Compound AND / OR:
"visibleWhen": {
"raw": "${age} >= 18 and ${gender} = 'male'",
"conditions": [
{ "field": "age", "operator": ">=", "value": "18" },
{ "field": "gender", "operator": "=", "value": "male" }
],
"logic": "and"
}Unparseable expression (complex XPath): conditions and logic are null, only raw is preserved.
No relevant column / empty cell: visibleWhen is null.
Supported operators: =, !=, >, <, >=, <=
component type
Use the component type to embed custom React components in your form. Define the component name and pass props via the parameters column using space-separated key=value pairs.
XLSForm row:
| type | name | label | parameters |
|---|---|---|---|
| component ConsentBanner | consent_banner | Informed Consent | variant=warning imageUrl=consent.png |
Output:
{
"type": "component",
"name": "consent_banner",
"label": "Informed Consent",
"componentName": "ConsentBanner",
"props": { "variant": "warning", "imageUrl": "consent.png" }
}Error handling
import { convertXLSForm, XLSFormParseError } from '@alex-roc/xlsform2json'
try {
const form = await convertXLSForm('./my-form.xlsx')
} catch (err) {
if (err instanceof XLSFormParseError) {
console.error(`Parse error at row ${err.rowNumber}: ${err.message}`)
}
}XLSFormParseError properties:
| Property | Type | Description |
|---|---|---|
| message | string | Human-readable description |
| rowNumber | number \| null | Excel row number (1-based), if applicable |
| column | string \| null | Column name, if applicable |
TypeScript types
All types are exported from the main entry point:
import type {
XLSFormJSON,
FieldDefinition,
ValidationDescriptor,
VisibleWhen,
VisibleWhenCondition,
ChoiceOption,
ChoicesMap,
// Specific field types:
TextField,
IntegerField,
DecimalField,
SelectOneField,
SelectMultipleField,
SelectOneFromFileField,
SelectMultipleFromFileField,
ComponentField,
} from '@alex-roc/xlsform2json'FieldDefinition is a discriminated union — narrow by field.type:
for (const field of form.fields) {
if (field.type === 'select_one') {
console.log(field.listName) // TypeScript knows this exists
}
if (field.type === 'component') {
console.log(field.componentName, field.props)
}
}v1 limitations
- Single language only. Multi-language
label::Englishcolumns are not supported yet. - No XPath constraint evaluation.
constraintis preserved as a raw string. - No
begin_group/begin_repeatnesting. Structural rows are skipped; all fields are returned flat. - No media columns (
image,audio,video).
License
MIT
