How does the Path type work in EK9 and what is it for?

← Getting Started · Ref: Q42

IMPORTANT: Path in EK9 is NOT a file system path. It is a data navigation path for traversing object graphs like JSON structures. If you need file system paths, use FileSystemPath instead. This is a critical distinction that developers from other languages often miss.

PATH IS FOR DATA NAVIGATION

Path uses the $? prefix to create compile-time checked navigation expressions for object graphs (JSON, nested records, data structures):

  simplePath <- $?.aKey
  nested <- $?.some.path.to.value
  fromArray <- $?[0].a-field
  multiDim <- $?.data[2][1].multi-dimensional.array
  complex <- $?.some.path.inc[0].array

FILESYSTEMPATH IS FOR FILES

FileSystemPath is the separate type for file and directory operations:

  filePath <- FileSystemPath("/home/user/file.txt")

It has methods like parent(), fileName(), extension(), exists(). Path has none of these because it navigates data, not file systems.

THE $? PREFIX

The path literal starts with $? followed by property names and array indices:

  Properties: $?.propertyName (dot-separated)
  Arrays: $?[index] (bracket notation)
  Mixed: $?.store.book[0].title
  Multi-dimensional: $?[2][1].field
  Hyphenated keys: $?[0].a-field

The compiler validates the syntax at compile time. Unlike Java's JsonPath strings or Python's dict key chains, typos and structural errors are caught before your code runs.

THE $ OPERATOR FAMILY

EK9 has three related operators that all use $ as a visual anchor but serve completely different purposes:

  $value      to-string conversion (any type to String)
  $$record    to-JSON serialization (record/class to JSON)
  $?.path     data path literal (compile-time checked navigation)

One $ is simple conversion, two $$ is structured output, $? is structured input navigation. This visual pattern makes the intent immediately recognizable in code.

ESCAPING $ IN INTERPOLATED STRINGS

Since $ triggers interpolation in backtick strings, use \$ to include a literal dollar sign:

  stdout.println(`\$ to-string: ${stringRep}`)
  stdout.println(`\$\$ to-JSON: ${jsonOfMe}`)
  stdout.println(`\$? path: ${navPath}`)

In normal double-quoted strings, $ does not need escaping. In backtick interpolation, $ and backtick must be escaped with backslash.

PATH CONCATENATION

The + operator joins paths:

  basePath <- $?.api
  extended <- basePath + $?.users

You can also append strings:

  withVersion <- basePath + "/v2"

PATH OPERATORS

  contains: check if path contains a substring
    fullPath contains "store"     true/false
  matches: match against a regular expression
    path matches /user.*/         true/false
  length: character count of the path
  ==, <>, <, >, <=, >=, <=>: comparison operators

WHY A BUILT-IN PATH TYPE?

Modern applications constantly navigate data structures: JSON API responses, configuration trees, nested records. In Java, you use string-based JsonPath ("$.store.book[0].title") with no compile-time checking. In JavaScript, optional chaining (obj?.store?.book?.[0]?.title) is concise but still unchecked. In Python, nested dict access (data['store']['book'][0]) throws KeyError at runtime.

EK9 makes data navigation a first-class type. Paths can be stored in variables, passed as parameters, composed programmatically, and validated at compile time. This is the same philosophy as RegEx being built-in rather than a library: if something is used everywhere, it should be part of the language with compile-time safety.

WHY SEPARATE FROM FILESYSTEMPATH?

Because they solve completely different problems:

  Path: navigates IN-MEMORY data structures (JSON, object graphs)
  FileSystemPath: navigates ON-DISK file system hierarchies

Mixing these concepts (as most languages do with a single 'path' type or string) leads to confusion. A JSON path $?.users[0].name has nothing to do with /home/users/name.txt. Different types, different operations, different safety requirements.

See Q43 (How do I escape characters in EK9 string interpolation?) for the complete rules on escaping $ in backtick strings. See Q37 (How do I work with strings in EK9?) for string interpolation basics.

Use 'ek9 -h Path' and 'ek9 -h FileSystemPath' to see the full APIs.

Example

defines module qa.path

  defines record
    CustomerDetail
      firstName <- String()
      lastName <- String()

      default CustomerDetail()

      CustomerDetail()
        ->
          firstName as String
          lastName as String
        this.firstName :=: firstName
        this.lastName :=: lastName

      default operator $$

      default operator ?

  defines program
    PathDemo()
      stdout <- Stdout()

      // === PATH LITERALS ===

      // $? prefix creates a compile-time checked path
      simplePath <- $?.aKey
      stdout.println(`Simple: ${simplePath}`)

      // Nested property access
      nested <- $?.some.path.to.value
      stdout.println(`Nested: ${nested}`)

      // Array indexing
      firstElement <- $?[0]
      stdout.println(`First element: ${firstElement}`)

      // Property from array element
      fromArray <- $?[0].a-field
      stdout.println(`From array: ${fromArray}`)

      // Multi-dimensional array access
      multiDim <- $?.data[2][1].multi-dimensional.array
      stdout.println(`Multi-dim: ${multiDim}`)

      // Complex path
      complex <- $?.some.path.inc[0].array
      stdout.println(`Complex: ${complex}`)

      // === DISTINGUISH FROM $ AND $$ ===

      // $value is the to-string operator
      anInt <- 42
      stringRep <- $anInt
      stdout.println(`\$ to-string: ${stringRep}`)

      // $$record is the to-JSON operator
      me <- CustomerDetail(firstName: "Steve", lastName: "Limb")
      jsonOfMe <- $$me
      stdout.println(`\$\$ to-JSON: ${jsonOfMe}`)

      // $?.path is the path navigation literal
      navPath <- $?.store.book[0].title
      stdout.println(`\$? path: ${navPath}`)

      // === PATH CONCATENATION ===

      basePath <- $?.api
      extended <- basePath + $?.users
      stdout.println(`Concatenated: ${extended}`)

      // Append string
      withString <- basePath + "/v2"
      stdout.println(`With string: ${withString}`)

      // === PATH OPERATORS ===

      // Length
      stdout.println(`Length: ${length nested}`)

      // Comparison
      path1 <- $?.alpha
      path2 <- $?.beta
      require path1 <> path2
      require path1 < path2
      stdout.println(`alpha <=> beta: ${path1 <=> path2}`)

      // Contains
      fullPath <- $?.store.book[0].title
      containsResult <- fullPath contains "store"
      stdout.println(`Contains 'store': ${containsResult}`)

      // Matches with RegEx
      matchPath <- $?.users
      matchResult <- matchPath matches /user.*/
      stdout.println(`Matches /user.*/: ${matchResult}`)

      // === HASHCODE ===

      hash <- #? simplePath
      stdout.println(`Hash: ${hash}`)

      // === UNSET PATH ===

      unsetPath <- Path()
      require ~unsetPath?
      stdout.println(`Unset isSet: ${unsetPath?}`)

      // === COPY ===

      copied <- Path()
      copied :=: nested
      require copied == nested
      stdout.println(`Copied: ${copied}`)

Common mistakes

E08180 — Record fields must be initialised at declaration. Declaring fields without initialisation triggers E08180. Use inline initialisation like 'String()' to create an unset-but-initialised field. See ek9 -h E08180 for details.

Incorrect:

firstName as String
      lastName as String

Correct:

firstName <- String()
      lastName <- String()
Other ways to ask this
  • What is the $? path literal in EK9?
  • How do I navigate JSON data structures in EK9?
  • What is the difference between Path and FileSystemPath in EK9?
  • How do the $, $$, and $? operators differ in EK9?

Coming from another language?

Java: JsonPath library with string expressions $.store.book[0].title, no compile-time checking, runtime exceptions on invalid paths, java.nio.file.Path for file system. JavaScript: native optional chaining obj?.store?.book?.[0], jq for CLI JSON queries, no compile-time validation, fs.path for files. Python: nested dict access data['store']['book'][0] with KeyError, jsonpath-ng library, pathlib.Path for files. Rust: serde_json Value indexing value["store"]["book"][0], jsonpath-rust crate, std::path::Path for files. Go: encoding/json with map[string]interface{} casting, gjson library for JSON paths, filepath package for files. C#: System.Text.Json JsonElement navigation, JsonPath.Net library, System.IO.Path for files. EK9: native $?.path literal with compile-time validation, Path type for data navigation, FileSystemPath type for files, clear separation of concerns.

Keywords: filesystempath, escape, data, dollar, navigation, object, start, compile, beginner, migrate, interpolation, path, graph, literal, property, array, json, first, file, intro