Fixing PDAL Pipeline JSON Schema Errors

PDAL aborts before it processes a single point: PDAL: Unable to parse pipeline, Couldn't create reader stage of type 'readers.laz', or Invalid option 'window_size' for stage 'filters.smrf'. Nothing was rasterized, and the traceback points at pipeline construction rather than execution. These are schema errors — the JSON you handed PDAL is well-formed text but does not describe a valid pipeline. This page maps each error class to its cause and gives one routine that validates a pipeline in Python before you ever call .execute().

Why PDAL rejects a pipeline before running it

A PDAL pipeline has a strict shape that is easy to violate in three ways, and the parser reports each differently. The first is the container form: PDAL accepts either a bare JSON array of stages, or an object with a single "pipeline" key whose value is that array. Wrapping the array in an object with any other top-level key, or nesting the "pipeline" key twice, yields Unable to parse pipeline because the parser cannot find the stage list where the schema says it lives. The second is stage identity: every stage names its type as "type": "readers.las", "filters.smrf", "writers.gdal", and PDAL looks that string up in a registry of compiled plugins. A typo, a wrong family (readers.laz does not exist — LAZ is read by readers.las), or a driver whose plugin was not built produces Couldn't create ... stage of type .... The third is option typing: each stage declares its options with expected value types, and passing a string where a number belongs ("resolution": "0.25"), an unknown option name (window_size instead of window), or the wrong JSON scalar triggers an invalid-option or conversion error.

Because these fail at construction, the fix is always to validate the pipeline object in isolation before running the expensive point processing — exactly the discipline the parent generating DSM and DTM from point clouds with PDAL workflow relies on so a schema typo never wastes a long run.

Minimal reproducible solution

The routine below builds the pipeline as a native Python dict (so JSON syntax is never hand-typed), serialises it with json.dumps, constructs pdal.Pipeline, and calls .validate(). In recent PDAL the constructor already parses and checks the schema, so a bad stage name or malformed container raises immediately with a precise message; wrapping it lets you surface that message and pinpoint the offending stage instead of reading a bare traceback.

import json
import pdal


def validate_pipeline(spec: dict) -> pdal.Pipeline:
    """Construct and validate a PDAL pipeline dict, raising a clear error.

    Catches the three schema faults: wrong container form, unknown stage
    type, and mistyped/unknown option values -- all before execute().
    """
    # 1. Container form: must be {"pipeline": [ ...stages... ]}.
    if "pipeline" not in spec or not isinstance(spec["pipeline"], list):
        raise ValueError("pipeline must be an object with a 'pipeline' array")

    # 2. Every stage must name a type string.
    for i, stage in enumerate(spec["pipeline"]):
        if isinstance(stage, dict) and "type" not in stage and "filename" not in stage:
            raise ValueError(f"stage {i} has neither a 'type' nor an inferable filename")

    text = json.dumps(spec)                 # never hand-type JSON
    pipeline = pdal.Pipeline(text)          # raises on unknown stage / bad option
    pipeline.validate()                     # schema check without processing points
    return pipeline


good = {
    "pipeline": [
        {"type": "readers.las", "filename": "cloud.laz"},   # LAZ read by readers.las
        {"type": "filters.smrf", "window": 18.0},           # 'window', a float
        {"type": "writers.gdal", "filename": "dtm.tif",
         "resolution": 0.25, "output_type": "idw"},         # numbers, not strings
    ]
}
validate_pipeline(good)                     # returns a validated Pipeline

The two guard clauses catch the container and missing-type faults with a readable message; pdal.Pipeline(text) catches unknown stage names and bad options via PDAL’s own registry. Note the three corrections baked into the good example: LAZ files use readers.las (not a non-existent readers.laz), the SMRF option is window (not window_size), and resolution is the number 0.25 (not the string "0.25"). Build every pipeline as a dict and these classes of error largely disappear, because Python’s json.dumps guarantees valid syntax and correct scalar types.

Edge-case matrix

Input variant Error PDAL raises Expected fix
Array wrapped as {"stages": [...]} Unable to parse pipeline Rename the key to "pipeline"
Bare array [{...}, {...}] none — this form is valid Accept it, or wrap in {"pipeline": [...]} for tooling
"type": "readers.laz" Couldn't create reader stage of type 'readers.laz' Use readers.las; it reads LAZ transparently
"resolution": "0.25" (string) option conversion / invalid-option error Pass the JSON number 0.25
"window_size": 18 on filters.smrf Invalid option 'window_size' Correct to window
Trailing comma in hand-typed JSON Unable to parse pipeline (JSON syntax) Build the dict in Python; never hand-type
Driver plugin not built (e.g. writers.ept) Couldn't create ... stage of type ... Install the plugin build or use an available writer

The readers.laz mistake is the most common because the file extension implies a stage that does not exist — PDAL keeps LAS and LAZ under one readers.las reader and detects compression from the file itself. The trailing-comma case is why the routine builds a dict rather than storing a .json string in the repository.

Verification snippet

After validation passes, confirm PDAL resolved the exact stages you intended by inspecting the pipeline it parsed — a stage silently defaulting to the wrong driver is caught here, before execution.

import json
import pdal

pipeline = validate_pipeline(good)
# pipeline.pipeline is the normalised JSON PDAL actually parsed.
parsed = json.loads(pipeline.pipeline)
types = [s.get("type") for s in parsed["pipeline"] if isinstance(s, dict)]
assert "readers.las" in types, "reader stage did not resolve to readers.las"
assert "writers.gdal" in types, "writer stage missing after parse"
print("validated stages:", types)

If .validate() raised, the message already names the offending stage and option; if it passed but types lists a stage you did not intend, an option name collided with a stage default — re-read the stage’s option list and correct the key.

When to escalate

Schema validation gets the pipeline to run; it does not guarantee the result is correct. Return to generating DSM and DTM from point clouds with PDAL when:

  • The pipeline validates and executes but produces zero points or an empty raster, which is a filter-logic problem (a too-strict filters.range), not a schema one — the parent guide’s troubleshooting covers it.
  • A stage you need reports as an unknown type even though the name is correct, meaning the plugin was not compiled into your PDAL build; reinstall PDAL from conda-forge so the driver is present.
  • The error surfaces only inside filters.hag_delaunay or the SMRF options during de-vegetation, in which case the tuning context is in removing vegetation from DTM ground surfaces.

Generating DSM and DTM from Point Clouds with PDAL