Repository navigation
Schema mapping: bind cells and columns to JSON schema properties #1
Copy link
Copy link
Open
Description
Goal
Let users map columns or cells to properties of an OSL JSON schema (e.g. Category:Item), so a spreadsheet can be treated as a tabular view of typed records, similar to how Excel lets you pick cell refs in a formula.
Builds on two features already in the codebase (Univer-migration branch):
- Label overlay: cells holding a UUID render as the page's human label.
- Autocompletion: configurable per-range SMW live-query popup; selecting a result writes the page's UUID.
What mapping looks like to the user
- User opens the editor and clicks a new toolbar button "Schema mapping".
- Dialog appears with two halves:
- Left: tree of properties from one or more selected Category schemas (e.g.
Category:Itemwithlabel,description,quantity.value,quantity.unit). - Right: live preview of which columns or cells are already mapped.
- Left: tree of properties from one or more selected Category schemas (e.g.
- To map a property: click it in the tree, then click a cell or column header in the sheet. The cell gets a colored corner marker and a tooltip with the property name.
- Each mapping persists in
metadata.schemaMappingson the workbook.
Data model (proposal)
{
"metadata": {
"schemaMappings": {
"sheetId-0": {
"categories": ["Category:Item"],
"bindings": [
{ "cell_range": "B:B", "propertyPath": "label" },
{ "cell_range": "C:C", "propertyPath": "quantity.value" },
{ "cell_range": "D:D", "propertyPath": "quantity.unit" }
]
}
}
}
}cell_rangeuses the same A1 syntax as the autocompletion feature. Namedcell_range(notrange) because the schema's ownrangeannotation already exists for property reference filtering, and we want to keep them distinct.propertyPathis dot-notation through the schema's nested objects and is unique inside the schema — fully identifies the target. The SMW property identifier travels in the schema's JSON-LD@context, no need to duplicate it here.- Compound properties (e.g. quantity = value + unit) are expressed as two bindings sharing a parent path; the relationship is clear from the paths themselves.
Integration with existing features
- Autocompletion: when a mapped property has a
rangeorsubclassof_rangein its schema annotation (accepts a UUID reference), the mapping should auto-configure an autocompletion entry for thatcell_range. The Category quick-pick already wires SMW queries from a category; same plumbing applies. - Label overlay: any cell mapped to a UUID-typed property automatically gets label rendering.
- Storage: extend
DataAdapter.toStorageFormat()and the load path to round-tripmetadata.schemaMappings.
Schema source
OSL stores schemas in the jsonschema slot of Category:* pages. Existing utilities to reuse:
mwjson.api.getPage()/getFilePage()for fetching slot contentmwjson.schemaclass for parsing, dereferencing$ref, walkingallOf, traversing properties- The postMessage bridge between iframe and parent (parent owns wiki API calls)
UX subquestions to settle before coding
- Tree view: list all properties flat, or grouped by sub-object?
- Required vs optional properties: visual indication?
- Multi-category: union the property sets, or sheets-per-category?
- Mapping mode: click-property-then-click-cell, or drag-and-drop?
- Visual marker for mapped cells: corner triangle (already used for labels), different color?
- Unmap UX: right-click on a mapped column?
Follow-up: export-to-JSON
Once mappings exist, add an "Export as JSON" button that walks the sheet row by row, builds JSON objects matching the schema, and either downloads them or creates wiki pages directly. Can be a separate issue.
Acceptance criteria
- Schema mapping dialog opens from a toolbar button while editing
- User can pick one or more Category schemas (autocomplete from the categories namespace)
- Properties of selected schemas appear in a navigable tree
- Clicking a property and then a cell creates a mapping
- Mapped cells get a distinct visual indicator
- Mappings persist across save and load
- Removing a mapping is reversible
- When a UUID-typed property is mapped, the corresponding cell_range automatically gains an autocompletion config
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
No labels
Activity
simontaurus commentedon May 27, 2026
Proposal: OO-LD schema, partial flatten, Excel and JSON-editor hybrid, with free placement
The generalisation that ties this issue, #2, and the array work together:
OO-LD schema, partially flattened, Excel plus JSON-editor hybrid UI. Excel handles flat scalar leaves. A JSON-editor popup/modal handles whatever is left under each flattening boundary (nested objects, residual arrays, autocomplete fields). Schema paths (
root.property[0]....) can be placed anywhere on the sheet, so the whole machinery reuses every existing schema definition: validation, defaults, RDF mapping via the JSON-LD@context, autocompletion viarange/subclassof_range, descriptions, titles, language variants.Built on
mwjson.util.flattenSchema/flatten/unflatten. The proposals below alignpropertyPathand the binding shape so that mapping is just "tell us which schema path lives at which spreadsheet cell", nothing more.This issue covers the framework, i.e. the data model the user manipulates by hand via the mapping dialog. Issue #2 then describes the template generator that produces a default mapping automatically from one or more category schemas.
Free placement
A mapping is just
cell_rangepaired withpropertyPath. Consequences:B2), a column (B:B), a row (2:2), a rectangle (B2:D2), or any A1 selection.The mapping carries metadata (
flatten_options) and per-binding layout metadata (blockfor grouped-blocks visual grouping, see #4); it never assumes the sheet's geometry.Hiding properties
There is no
hiddenflag on a binding. Hiding a property from the sheet is just not creating a binding for it. The property still exists in the record (filled with the schema'sdefault, autogenerated, or left absent if optional), andmwjson.util.flatten/unflattenround-trip unaffected paths through untouched.Typical reasons to leave a property unmapped:
defaultin the schema (the default fills in automatically on export),uuidorcreated_at(filled by the wiki layer on save),Removing a mapping later (delete the binding) effectively hides the column without losing any underlying record data; the property still round-trips through
flatten/unflattenif any of the other bindings happen to set or read it. Re-adding a binding for the samepropertyPathlater is non-destructive.The auto-generator in #2 uses this same mechanism: rather than emitting a
hiddenflag, it simply skips bindings for properties it would otherwise emit.Example layouts
For a category with
name,label: [{text, lang}],subclass_of: [Category:Category],metadata: {source, imported_at}, the same workbook metadata can drive very different sheet layouts. The four below all use the same binding shape and the sameflatten/unflattenround-trip.Standard tabular layout (one record per row)
{source: "import.csv"}{source: "manual"}Bindings (excerpt):
[ { "cell_range": "A2:A1000", "propertyPath": "name" }, { "cell_range": "B2:B1000", "propertyPath": "label[0].text" }, { "cell_range": "C2:C1000", "propertyPath": "label[0].lang" }, { "cell_range": "F2:F1000", "propertyPath": "subclass_of" }, { "cell_range": "G2:G1000", "propertyPath": "metadata" } ]The cell editor for each cell is derived from the resolved sub-schema (see below):
nameis a string scalar,subclass_ofis anarray<uuid>so it opens a multi-select picker or the generic json-editor (user preference),metadatais an object so it opens a json-editor modal.Pivot layout (one record per column)
Bindings:
[ { "cell_range": "B1:Z1", "propertyPath": "name" }, { "cell_range": "B2:Z2", "propertyPath": "label[0].text" }, { "cell_range": "B3:Z3", "propertyPath": "label[0].lang" }, { "cell_range": "B4:Z4", "propertyPath": "subclass_of" } ]Single-record sheet (one row of values driving a calculation)
Bindings:
[ { "cell_range": "B1", "propertyPath": "name" }, { "cell_range": "B2", "propertyPath": "mass.value" }, { "cell_range": "B3", "propertyPath": "mass.unit" }, { "cell_range": "B4", "propertyPath": "density.value" } ]Multi-section sheet
The mapping has two sets of bindings, one for each section, against the two relevant category schemas. The "===" header rows are plain cells that carry no binding.
Cell editor derived from the resolved sub-schema
The binding does not carry a
renderfield. The cell editor is derived from the sub-schema atpropertyPath, resolved on demand by walking the loaded category schema (seeresolveSubSchemabelow):string,number,integer,boolean,null: scalar cell (Excel side). The schema'sformatannotation drives a widget pick:date-> date picker,date-time-> datetime picker,email-> email validation,time-> time picker, etc.enum-> dropdown.range/subclassof_range-> autocompletion against an SMW query (existing plumbing).objectorarray: cell holds the sub-tree, double-click opens a json-editor popup/modal driven by that sub-schema. The same plumbing (validation, autocompletion inside the modal, defaults) the rest of OSL already uses applies.For the array case, the json-editor has several presentation choices that don't change the data model:
array<uuid>orarray<{$ref}>: render as multi-select chips in the cell, popup is the same chip picker the rest of OSL uses.array<string|number>: render as delimited text (comma) in the cell, popup is a simple list editor.array<complex object>: render as a "sub-sheet" link in the cell, popup is the linked sheet.Whether to apply the specialised presentation or fall back to the generic json-editor is a UI preference, not a property of the binding. A workbook-level
ui_preferencessection (or global user setting) can choose, e.g. "always use multi-select chips forarray<uuid>", with the json-editor as the universal fallback if the user prefers consistency.Path syntax (canonical
flattenSchemaoutput)With the default options (
array_index_notation: "[0]",notation: "dot"):name: scalarquantity.value,quantity.unit: object leaves (object always recurses)label[0].text,label[0].lang,label[1].text,label[1].lang: array expanded witharray_length: { "label[*]": 2 }subclass_of: array kept as-is whenarray_length["subclass_of[*]"]is undefined; the cell holds the whole arraymetadata: object whose path appears inflatten_options.stop_at_paths; the cell holds the whole sub-objectSame paths the issue #2 generator emits, same paths an OSL Property form would build, same paths the JSON-LD
@contextresolves to RDF properties. A record moves freely between form and sheet without translation.resolveSubSchemaAdvantages:
@context, validation, defaults, autocompletion). Editing the Category page propagates to every workbook that binds to it.Trade-off: opening a cell editor needs the category schema loaded. Already the case for autocompletion and label resolution, so no new network dependency. If the schema is unreachable the cell falls back to a raw JSON editor with a warning.
Binding shapes
Scalar leaf (Excel side of the hybrid):
{ "cell_range": "B:B", "propertyPath": "name" }Grouped-block leaf (still Excel side, just visually grouped, see #4):
{ "cell_range": "C:C", "propertyPath": "label[0].text", "block": [{ "groupId": "label", "pathPattern": "label[*]", "index": 0, "subProp": "text", "bannerColor": "#e3f2fd" }] }Flattening boundary (cell holds a sub-tree, json-editor by default):
{ "cell_range": "G:G", "propertyPath": "subclass_of" }{ "cell_range": "I:I", "propertyPath": "metadata" }That is the entire binding shape.
cell_range,propertyPath, and optionallyblockfor grouped-blocks layout (see #4). Everything else is derived.Sheet-level flatten config
{ "metadata": { "schemaMappings": { "sheetId-0": { "categories": ["Category:Item"], "flatten_options": { "array_index_notation": "[0]", "notation": "dot", "array_length": { "label[*]": 2, "subclass_of[*]": null, "tags[*]": null }, "stop_at_paths": ["metadata"] }, "bindings": [ ... ] } } } }array_length: number: array is flattened to that depth, leaf bindings emitted per index.array_length: null(or absent): array stays as array; the path itself is a binding.stop_at_paths: object subtrees re-collapsed afterflattenSchemaran (the upstream function always recurses into objects).A separate
ui_preferencesblock (workbook or user level) can choose specialised vs. generic renderers per sub-schema shape; it is orthogonal to the bindings.Round-trip helpers (already exist)
Export row to record:
Values at boundary paths are whole sub-trees (arrays/objects);
unflattenplaces them at the right path. The json-editor modal commits sub-trees back into the cell as JS objects/arrays, so no serialisation tricks needed.Load record to row:
For boundary bindings,
flattenwill not recurse past the boundary; the whole sub-tree lands in one cell.RDF and
@contextreuseA schema mapping is by construction also an RDF mapping. Each
propertyPathis a JSON path, and JSON-LD's@context(already present on every OSL category schema) maps each path to an RDF predicate. Once a sheet has bindings, exporting the rows as RDF triples is mechanical:No separate RDF-mapping layer for the sheet, it is the same mapping the form, the API, and the wiki page already use.
Required and default
flattenSchemapropagatesrequired(up tominItemsfor expanded array items) and distributesdefaults. For boundary cells, the resolved sub-schema's ownrequired/defaultapply inside the JSON-editor modal. Required leaves get a visual marker; defaults pre-fill empty cells.Open questions
propertyPathstrings against the currentflattenSchema(schema, options)output at load time? Catches drift between sheet and schema.array_length["label[*]"]drops from 3 to 2, do we delete the orphan columns automatically, or warn first?resolveSubSchemacache across cell edits? Memoise per(category, propertyPath)and invalidate on category-schema reload?ui_preferenceslive (workbook, user, both)? And which presentation defaults ship out of the box?