Exercise 7: Transforms
So far every block has described data in its native form — you define a schema, annotate it semantically, and validate it. Transforms add another layer: the ability to declare how to convert data conforming to the block into a different representation. The conversion logic travels with the block definition, so consumers always have access to it.
Transforms are declared in transforms.yaml. Each transform specifies:
- An
idanddescription - A
type:jq(JSON-to-JSON transformation),sparql-update(RDF graph manipulation), orxslt(XML transformation) - Either a
refpointing to a file containing the transform code, or an inlinecodeblock
When the postprocessor finds transforms.yaml, it applies each transform to
every example in the block and records the input/output pairs. The viewer
displays these under the Transforms tab.
Step 1: Create the exercise files
Create the directory _sources/exercise7/ with the following structure:
_sources/exercise7/
├── bblock.json
├── schema.yaml
├── context.jsonld
├── examples.yaml
└── transforms/
├── prefix-keys.jq
└── prefix-predicates.sparql
bblock.json:
{
"name": "Exercise 7: Transforms",
"abstract": "Declaring jq and SPARQL Update transforms that convert examples to other representations.",
"itemClass": "schema",
"status": "under-development",
"dateTimeAddition": "2024-01-01T00:00:00Z",
"version": "0.1",
"dateOfLastChange": "2024-01-01"
}
schema.yaml:
type: object
properties:
one:
type: number
two:
type: number
string:
type: string
context.jsonld — needed for the SPARQL Update transform, which operates
on the uplifted RDF graph:
{
"@context": {
"ex": "http://example.com/",
"one": "ex:hasOne",
"two": "ex:hasTwo",
"string": "ex:hasString"
}
}
examples.yaml:
examples:
- title: Example for transforms
snippets:
- language: json
code: |
{
"one": 1,
"two": 2,
"string": "value"
}
transforms/prefix-keys.jq — a jq program that prepends "PREF" to
every key in the input object:
walk(if type == "object" then with_entries( .key |= "PREF\(.)" ) else . end)
transforms/prefix-predicates.sparql — a SPARQL Update query that
replaces the URI prefix of every predicate in the uplifted RDF graph:
DELETE { ?s ?p ?o }
INSERT { ?s ?p2 ?o }
WHERE {
?s ?p ?o
FILTER(STRSTARTS(STR(?p), "http://example.com/"))
BIND(URI(REPLACE(STR(?p), "^http://example.com/", "urn:example:ex#")) as ?p2)
}
Run the postprocessor. Navigate to the Exercise 7 block. There is no
Transforms tab — no transforms.yaml has been defined yet.
Step 2: Add the transforms file and rebuild
Create _sources/exercise7/transforms.yaml:
transforms:
- id: prefix-keys
description: Adds a "PREF" prefix to all object keys.
type: jq
ref: transforms/prefix-keys.jq
- id: prefix-predicates
description: Changes the "http://example.com/" prefix of every predicate to a URN
type: sparql-update
ref: transforms/prefix-predicates.sparql
- id: inline-transform
description: A transform's code can also be defined inside the transform's object
type: jq
code: |
.test = "test"
Rerun the postprocessor. Navigate to the Exercise 7 block and open the Transforms tab. For each declared transform, the viewer shows the input example and the transformed output side by side.
For the prefix-keys jq transform, the input:
{ "one": 1, "two": 2, "string": "value" }
becomes:
{ "PREFone": 1, "PREFtwo": 2, "PREFstring": "value" }
For the prefix-predicates SPARQL Update transform, the RDF graph is modified
in place: any triple whose predicate URI starts with http://example.com/ is
rewritten to use a urn:example:ex# prefix instead.
The inline-transform example shows that the transformation code does not have
to live in a separate file — it can be declared directly in transforms.yaml
with the code field.
How it works
jq transforms
jq is a lightweight, powerful command-line JSON
processor. A jq transform receives the example JSON as input and produces a new
JSON document as output. The walk function used in prefix-keys.jq is a
built-in that recursively traverses the input structure.
jq transforms are applied to the raw JSON input before semantic uplift. They are suitable for restructuring, filtering, or augmenting JSON data.
SPARQL Update transforms
A SPARQL Update transform receives the uplifted RDF graph of the example (not
the raw JSON) and modifies it in place using SPARQL DELETE/INSERT operations.
This is appropriate when the transformation needs to operate on the semantic
meaning of the data — for example, rewriting URIs, inferring new triples, or
removing triples that match a pattern.
SPARQL Update transforms require a context.jsonld to be present (so the
postprocessor can uplift the JSON to RDF first).
Inline code vs file references
Both ref (pointing to a .jq, .sparql, or .xsl file) and inline code
are supported. File references are preferable for longer transforms that benefit
from syntax highlighting and independent version history. Inline code is
convenient for short, self-contained transforms.
Advanced example: CityJSON to TopoFeature
The following is an observational exercise — there is nothing to edit. It demonstrates what a production-grade jq transform looks like when the source data requires significant structural manipulation.
The exercise uses CityJSON files as input — a compact format for 3D city models where geometry is expressed as indexed references into a shared vertex array. The transform converts these into GeoJSON features using the TopoFeature topology model, which preserves the shared vertex structure rather than duplicating coordinates across adjacent faces.
The transforms.yaml for this block defines a single transform:
transforms:
- id: topo-feature
description: Transforms CityJSON geometries and vertices to GeoJSON / TopoFeature features.
type: jq
ref: transforms/topo-feature.jq
The topo-feature.jq transform (74 lines) handles:
- CityJSON type mapping to TopoFeature geometry types
- Recursive processing of nested boundary references
- CityObject semantic surface classifications (roof, wall, ground surface)
- Vertex coordinate transformation from the CityJSON array
- Extent metadata mapping
To try this exercise, you can find the complete set of files — including the
CityJSON input examples and the full topo-feature.jq transform — in the
bblocks-tutorial repository
under _sources/exercise8/.
After running the postprocessor, open the Transforms tab for the block. The viewer shows each CityJSON input alongside its TopoFeature output, making it straightforward to verify that the transformation is correct by comparing the two representations.
This exercise illustrates that transforms are not limited to trivial modifications — a single jq file can express a complete, structurally complex data conversion, and the postprocessor will apply it automatically to every example.