HierarchicalUtils.jl

JsonGrinder.jl uses HierarchicalUtils.jl which brings a lot of additional features.

using HierarchicalUtils

Let's say we have a complex Schema, which we want to further inspect:

julia> using JSON
julia> jss = JSON.parse("""[ { "a": { "b": "foo", "c": [5, 6] }, "d": "bar" }, { "d": "baz" }, { "a": { "c": [] }, "b": "foo" } ]""");
julia> sch = schema(jss)DictEntry 3x updated ├── a: DictEntry 2x updated ├── b: LeafEntry (1 unique `String` values) 1x updated ╰── c: ArrayEntry 2x updated ╰── LeafEntry (2 unique `Real` values) 2x updated ├── b: LeafEntry (1 unique `String` values) 1x updated ╰── d: LeafEntry (2 unique `String` values) 2x updated

In small enough schema, all types of nodes are visible, but it gets more complicated if the schema does not fit your screen. Let's see how we can use HierarchicalUtils.jl to programmatically examine sch.

First, the whole tree (regardless of the display area size) can be printed with

julia> printtree(sch)DictEntry 3x updated
  ├── a: DictEntry 2x updated
  │        ├── b: LeafEntry (1 unique `String` values) 1x updated
  │        ╰── c: ArrayEntry 2x updated
  │                 ╰── LeafEntry (2 unique `Real` values) 2x updated
  ├── b: LeafEntry (1 unique `String` values) 1x updated
  ╰── d: LeafEntry (2 unique `String` values) 2x updated

Callling with trav=true enables convenient traversal functionality with string indexing:

julia> printtree(sch, trav=true)DictEntry [""] 3x updated
  ├── a: DictEntry ["E"] 2x updated
  │        ├── b: LeafEntry (1 unique `String` values) ["I"] 1x updated
  │        ╰── c: ArrayEntry ["M"] 2x updated
  │                 ╰── LeafEntry (2 unique `Real` values) ["O"] 2x updated
  ├── b: LeafEntry (1 unique `String` values) ["U"] 1x updated
  ╰── d: LeafEntry (2 unique `String` values) ["k"] 2x updated

This way any element in the schema is swiftly accessible, which may come in handy when inspecting model parameters or simply deleting/replacing/inserting nodes in the tree. All tree nodes are accessible by indexing with the traversal code:

julia> sch["O"]LeafEntry storing 2 unique `Real` values:
  5 => 1
  6 => 1

The following two approaches give the same result:

julia> sch["O"] ≡ sch[:a][:c].itemstrue

We can iterate over specific nodes in the schema. Let's for example collect all its leaves:

julia> LeafIterator(sch) |> collect4-element Vector{LeafEntry}:
 LeafEntry (1 unique `String` values)
 LeafEntry (2 unique `Real` values)
 LeafEntry (1 unique `String` values)
 LeafEntry (2 unique `String` values)

or all DictEntry nodes:

julia> TypeIterator(DictEntry, sch) |> collect2-element Vector{DictEntry}:
 DictEntry
 DictEntry

We can for example get all traversal codes for nodes matching a given predicate:

julia> codes = pred_traversal(sch, n -> n.updated ≥ 2)5-element Vector{String}:
 ""
 "E"
 "M"
 "O"
 "k"

We can even get Accessors.jl optics:

julia> optic = code2lens(sch, "M") |> only(@o _.children[:a].children[:c])

which can be used to access the nodes too (as well as many other operations):

using Accessors
julia> getall(sch, optic) |> onlyArrayEntry 2x updated
  ╰── LeafEntry (2 unique `Real` values) 2x updated
Further reading

For the complete showcase of possibilities, refer to the HierarchicalUtils.jl manual.