Wishlist: Better support for nested JSON tiddlers

A little background, to begin with: I recently started exploring @Mohammad’s SearchWikis plugin, which creates an index of (a subset of) all the tiddlers in a given wiki.

  • Each wiki index is stored as a single application/json tiddler.
  • Within the JSON tiddler, each tiddler title is an index key, and each value records a few field values from that tiddler. The default index-value used by the JSON generator is $(title)$|$(caption)$|$(tags)$.
  • Thus, the JSON output might look like this:
{
    "\"A free, open source wiki revisited\" by Mark Gibbs, NetworkWorld": "\"A free, open source wiki revisited\" by Mark Gibbs, NetworkWorld||Articles",
    "\"A Thesis Notebook\" by Alberto Molina": "\"A Thesis Notebook\" by Alberto Molina||Examples",
    "\"ATWiki\" by Lamusia Project": "\"ATWiki\" by Lamusia Project||Resources",
    "\"BJTools\" by buggyj": "\"BJTools\" by buggyj||Resources",
    "\"BrainTest - tools for a digital brain\" by Danielo Rodriguez": "\"BrainTest - tools for a digital brain\" by Danielo Rodriguez||Resources"
}

This JSON tiddler gets imported into a “master” wiki, where Mohammad uses split[|] to retrieve the individual field contents from each index value:

<$vars inTitle={{{[<result>split[|]nth[1]]}}}
       inCaption={{{[<result>split[|]nth[2]]}}}
  	   inTags={{{[<result>split[|]nth[3]]}}} >

This is all very clever, and it works well, but when I started hacking, slashing, and otherwise adapting the concept for my own purposes, I started thinking that there had to be a better way to write and access these values. A nested JSON structure would take a few more characters, but it would be more legible at a glance…

{
    "\"BJTools\" by buggyj": {
        "title": "\"BJTools\" by buggyj",
        "tags": "Resources"
    }
}

And surely, (I thought naively1) it would be easier to access named values. After all, we have purpose-built operators like jsonget!

If you’ve ever tried to work with complex JSON structures in TW, you may already have guessed that this didn’t go as smoothly as I’d hoped.

Pain points in more or less the order I encountered them:

  1. Right off the bat: There’s no obvious way to create a nested JSON in the first place.
  • Adding indexes to a flat structure is very simple: just use $action-setfield. For instance:
<$list filter="[tag[HelloThere]]" variable="index">
	<$action-setfield $tiddler="My JSON Index" $index=<<index>> $value={{{ [<index>get[created]] }}} />
</$list>

yields

{
    "A Gentle Guide to TiddlyWiki": "20150325170720682",
    "Discover TiddlyWiki": "20140904121000000",
    "Some of the things you can do with TiddlyWiki": "20140904090300000",
    "Ten reasons to switch to TiddlyWiki": "20140904085700000",
    "Examples": "20140320230543190",
    "What happened to the original TiddlyWiki?": "20140904085100000",
    "Funding TiddlyWiki": "20221204165636777",
    "Open Collective": "20221204165636777"
}
  • However, it doesn’t seem to be possible to set or modify nested indexes without rewriting the entire text field of the JSON tiddler. Here’s an example by @Scott_Sauyet:

I did a little experimenting with this approach and found:

  • It doesn’t seem to be possible to use jsonset unless you’re starting with an existing JSON tiddler SomeTid.
  • It’s difficult to iterate over a $list as I did with $action-setfield above. I’d hoped I could use a :map run to add X field/value pairs for each of Y tiddlers, but I couldn’t figure out how to get this to work with jsonset.

Eventually I gave up on this approach and tried to build the structure I wanted without any JSON-specific operators. This is horribly inelegant, but it does seem to work: JSON Builder.tid (1.3 KB)

\define indexTiddler() JSON test
\define include() [tag[HelloThere]]
\function get.fields() created modified caption tags :filter[<..currentTiddler>has{!!title}] :map[field.index[]]

\function make.string() =[["]] [all[]] =[["]] +[join[]]
\function make.key() [{!!title}make.string[]] =[[: ]] +[join[]]

\define lb() {
\define rb() }

\function tiddler.index()
[make.key[]] [<lb>] +[join[]]
[get.fields[]join<comma-break>]
[<rb>] +[join<lbr>]
\end

\function field.index()
[make.key[]] [<..currentTiddler>get{!!title}make.string[]] +[join[]]
\end

<$link to=<<indexTiddler>> />

<$let
	lbr="""
"""
	comma-break=""",
"""

	text=```{
${ [subfilter<include>] :map[tiddler.index[]] +[join<comma-break>] }$
}```>

<$button>
	write text
	<$action-setfield $tiddler=<<indexTiddler>>
		type="application/json"
		text={{{ [<text>] +[format:json[4]] }}} />
</$button>

<pre>
	<$transclude $variable=text $mode=block />
</pre>

</$let>

`{{{ [[JSON test]indexes[]] }}}`

{{{ [[JSON test]indexes[]] }}}

`{{{ [[JSON test]get[text]jsonget[Examples],[created]] }}}`: {{{ [[JSON test]get[text]jsonget[Examples],[created]] }}}

`{{{ [[JSON test]get[text]jsonget[Examples],[tags]] }}}`: {{{ [[JSON test]get[text]jsonget[Examples],[tags]] }}}

You can see some (working) filter tests at the bottom of that code, which was where I had my second unwelcome revelation:

  1. jsonget does not work like getindex.
  • getindex expects its input to be a title: [[$:/palettes/Vanilla]getindex[background]]
  • jsonget expects its input to be a complete JSON string — so if you’re working with a JSON tiddler, you need to use get[text] before you can retrieve a specific value.

And this felt so inefficient for regular use in a filter — particularly when I’d hoped to use it with large JSON tiddlers like the ones produced by SearchWikis — that I decided Mohammad had had good reasons to choose the format he did, and gave up on my revisions entirely.

What I’d love to see in a future version of TW:

  • An <$action-setfield $index= ... equivalent that makes it easy to add nested, programmatically-determined key-value pairs — whether the target tiddler already exists or not.
    • Or (ideally?) an extension of the existing $action-setfield widget?
  • Similarly, an easier alternative to jsonget. IMO, it’d be most intuitive to extend getindex to take multiple parameters
    • Current solution: [[JSON test]get[text]jsonget[Examples],[tags]]
    • Ideal solution: [[JSON test]getindex[Examples],[tags]]

1 I know very little about JS; I’ve never really used it for anything more complicated that storing data in TW, and even then I generally let $action-setfield do the work for me. It’s very possible that I’m missing some easy solutions (though I did search the docs, this forum, and the top Google results I found, and discovered that I’m not the first to struggle with this, and none of the previous topics seemed to include any satisfactory solutions…) If so, please tell me! I’d love to make this work.

2 Likes

I came accross these limitiationd in the past, people often proposed workarounds, however I belive we need a set of ‘primatives’ for reading and writting complex (heirachical) JSON files after all this is a key way to intergrate data and other solutions into tiddlywiki, via JSON files.

  • A related requirement is to read/write/extract/insert/delete tiddlers into and out of the core JSON format or others.
  • Ensuring the same could be possible with other formats is importiant (CSV etc…)

It should be a simple matter or building on top of current mechanisiums alreqady used in tiddlywiki.

  • It is importiant in this area to look at what “JSON Mangler” plugin does.

I neglected to mention it in my OP, but I did investigate JSON Mangler (link to archive.org version ca. 2024, as @joshuafontany’s site seems to be down; you may need to download a copy for full functionality). But, aside from its age1, I think it doesn’t quite lend itself to the degree of parameterization I’d expect in modern TW.

Most notably, Joshua seems to be using a path-style notation for nested values, e.g. getindex[this/is/a/nested/index/5]. This is nicely compact but makes it difficult to use variables/transclusions in the path name. I’d really like to be able to do something like getindex<var>,[is],[a],{!!type},[index], for instance… but if I were using the plugin, I think I’d need to compose the index path as a separate function/filter run:

\function my.index() [<var>] is a [{!!type}] index +[join[/]]

[[Data]getindex<my.index>]

This is admittedly picky, but it’s the sort of convenience I’d hope for in a potential core feature.

However, to give credit where it’s due, JSON Mangler does let me do more or less what I wanted on the index-creation front:

<$button>set
<$list filter="[tag[Test]]" variable="tiddler">
	<$list filter="caption created modified tags" variable="field">
		<$action-setfield $tiddler="My Index" $index={{{ [<tiddler>] [<field>] +[join[/]] }}} $value={{{ [<tiddler>get<field>] }}} />
	</$list>
</$list>
</$button>

So thank you for inspiring me to take another look! Perhaps what it needs most is simply slightly more guidance on how best to use it… which is, I suppose, the perennial TW problem. :wink:


1 JSON Mangler dates back to 2019 — its most recent version was contemporaneous with TW 5.1.23, for reference — so it’s no surprise that it doesn’t quite “fit” with 5.3+ syntax!

It also modifies some core tiddlers; I haven’t yet investigated whether this creates any issues in a “modern” wiki.

1 Like

Great,this is also my need.

Hi Emily, have you tried looking at the :reduce filter run prefix? That would allow you to carry an accumulator (the final JSON expression that you want to build up) through a series of iterations. Each iteration could use jsonset[] to add a JSON property (which may be built in a subfilter or such). At the end, you’d assign the accumulator to your tiddler text field, having to set this field only once.

That’s an interesting approach! I always forget about :reduce.

It still doesn’t sit right with me that jsonset expects you to rebuild the entire text field every time you use it, though. And to be honest, I doubt I’ll explore it much further. The JSON Mangler plugin works well enough on the index-setting front that unless it breaks my wiki in some unforeseen way, I’ll probably stick with it unless/until its expanded $action-setfield functionality makes it into the core.

I do still think we need these core enhancements, though. JSON Mangler is the best option available right now, but it’s increasingly obscure, overwrites several core tiddlers (based on a 5+ year old version of TW), and the documentation is daunting — and I already knew what I was looking for!

1 Like

Hi @etardiff thank you for bringing this up. The JSON operators are incomplete in a number of respects, and some common operations are complex to express. I’d certainly like to improve things.

Taking your suggestions, perhaps:

  • An <$action-setjson> widget that provides a simple, direct way to store data at an arbitrary index within a JSON tiddler. The path could be expressed as numeric parameters <$action-setjson $tiddler=<<mytiddler>> 0=<<firstindex>> 1={{SecondIndex}} $value=<<myvalue>>/>
  • An extension to the getindex operator to allow nested indexes to be specified

I think we can treat the fact that [[{}]jsonset[d],[e],[Jaguar]] doesn’t work as expected as a bug, and it should be fixable without affecting backwards compatibility.

2 Likes

This discussion is way above my head, but the KeyValues plugin works with regex Operator rules.
I am not sure if you tried it or used it to find a solution or whether it is of any use to you, but it is worth mentioning.

Here is how you would use the :reduce prefix to build a JSON string that should be close to what you want:

\function .currentFieldContent() [<..currentTiddler>get<currentTiddler>]
\function .singleTiddlerJson() [subfilter<fieldSubfilter>] :reduce[<accumulator>jsonset<currentTiddler>,<.currentFieldContent>]

<$let tiddlerList={{{ [first[5]format:titlelist[]join[ ]] }}}
	  fieldSubfilter="[fields[]] -[[text]] -[[title]]"
      jsonData={{{ [enlist<tiddlerList>] :reduce[<accumulator>jsonset:json<currentTiddler>,<.singleTiddlerJson>] }}}
      >
  <$codeblock code=<<jsonData>> />
</$let>

The inner :reduce accumulates all fields (except text and title) into a JSON block, and the outer :reduce accumulates these inner JSON blocks as a property of the tiddler title.

On tiddlywiki.com the result looks like this (first 5 tiddlers):

{
    "\"A free, open source wiki revisited\" by Mark Gibbs, NetworkWorld": {
        "created": "20160204225047445",
        "modified": "20160204225307847",
        "tags": "Articles",
        "type": "text/vnd.tiddlywiki",
        "url": "http://www.networkworld.com/article/3028098/open-source-tools/tiddlywiki-a-free-open-source-wiki-revisited.html"
    },
    "\"A Thesis Notebook\" by Alberto Molina": {
        "created": "20130302085406905",
        "modified": "20130302084548184",
        "tags": "Examples",
        "url": "http://tesis.tiddlyspot.com/"
    },
    "\"ATWiki\" by Lamusia Project": {
        "created": "20171219171531482",
        "modified": "20210106151026834",
        "tags": "[[Other Resources]]",
        "type": "text/vnd.tiddlywiki",
        "url": "https://lamusia.github.io/#ATWiki"
    },
    "\"BJTools\" by buggyj": {
        "created": "20140315085406905",
        "modified": "20210106151026926",
        "tags": "[[Community Plugins]]",
        "type": "text/vnd.tiddlywiki",
        "url": "http://bjtools.tiddlyspot.com"
    },
    "\"BrainTest - tools for a digital brain\" by Danielo Rodriguez": {
        "created": "20140315085406905",
        "modified": "20210106151026982",
        "tags": "[[Other Resources]]",
        "type": "text/vnd.tiddlywiki",
        "url": "http://braintest.tiddlyspot.com/"
    }
}

(I have added linebreaks to make it more readable).

You can modify the tiddlers you want listed in the definition of tiddlerList, and the included fields in fieldSubfilter. Instead of $codeblock you could use $action-setfield to assign this jsonData to the text of a tiddler (everything at once).

No need for (unmaintained) plugins.

PS: If you want to store all the fields in the JSON structure, you could use <jsontiddler> instead of <.singleTiddlerJson> and make use of the respective core macro.

2 Likes

I think that could be a WikiText widget that makes use of the json* filter operators in a more accessible way. It’d be called <$action.setjson>, then.

Most of the fiddling would probably be to verify that all intermediate index levels are given, and maybe to figure out how many levels were given and to handle that in a flexible way.

In Emily’s case, inside a $list, this would still lead to a lot of tiddler writes.

1 Like

I am continually frustrated with JSON in TW. If I try hard enough, I can usually get things done, but it’s non-trivial.

You can do it with full paths to your nodes: [<myJSONstring>jsonset[grandpa],[mom],[me],[over21],[yes]]. But you need to set all intermediate nodes before you add a property to them. The above won’t work unless the object your JSON string represents already has a grandpa property, which has a mom property, which has a me property that is an object:

<$let myJSONstring='{"grandpa": {"mom": {"me": {"name": "Scott"}, "bro": {"name": "Michael"}}}}'>
  <pre><code><$text text={{{[<myJSONstring>jsonset[grandpa],[mom],[me],[over21],[yes]] +[format:json[4]] }}} /></code></pre>
</$let>

yielding

{
    "grandpa": {
        "mom": {
            "me": {
                "name": "Scott",
                "over21": "yes"
            },
            "bro": {
                "name": "Michael"
            }
        }
    }
}

But yes, that is awkward as hell.

This is because of what JSON actually is. It is not a data structure; instead it is a string representation of a data structure. That sounds subtle, but the point is important. Although tiddlers are pretty simple data structures (Hash Maps / Dictionaries), most everything else we deal with in TW is a string or a list of strings.

JSON was designed as a transport format: a way to share nested data structures between different systems. But working with JSON in programming languages usually involves parsing JSON into a more sophisticated data structure on receipt, manipulating that structure as needed, and serializing that data structure back into a string before passing it along. In TW, there’s more friction in this than in many languages. But even if there were no friction, the model would still involve converting from a string, making changes, and converting back.

Mostly to sharpen my TW-JSON skills, I tried this, and the results are not too bad:

<$button>Extract
<$action-createtiddler $basetitle="MyExtract" type="application/json" text="{}" >
  <$let tid=<<createTiddler-title>> >
    <$list filter=[tag[HelloThere]]>
      <$action-setfield $tiddler=<<tid>> $field="text" $value={{{ 
        [<tid>get[text]jsonextract[]] 
        +[jsonset:object{!!title}]
        +[jsonset{!!title},[title],{!!title}]
        +[jsonset{!!title},[tags],{!!tags}]
        +[jsonset{!!title},[caption],{!!caption}]
        +[format:json[4]]
      }}} />
    </$list>
    <$action-navigate $to=<<tid>> />
  </$let>
</$action-createtiddler>
</$button>

which yields this:

{
    "A Gentle Guide to TiddlyWiki": {
        "title": "A Gentle Guide to TiddlyWiki",
        "tags": "HelloThere",
        "caption": ""
    },
    "Discover TiddlyWiki": {
        "title": "Discover TiddlyWiki",
        "tags": "HelloThere",
        "caption": ""
    },
    "Some of the things you can do with TiddlyWiki": {
        "title": "Some of the things you can do with TiddlyWiki",
        "tags": "HelloThere",
        "caption": ""
    },
    "Ten reasons to switch to TiddlyWiki": {
        "title": "Ten reasons to switch to TiddlyWiki",
        "tags": "HelloThere",
        "caption": ""
    },
    "Examples": {
        "title": "Examples",
        "tags": "HelloThere Community",
        "caption": ""
    },
    "What happened to the original TiddlyWiki?": {
        "title": "What happened to the original TiddlyWiki?",
        "tags": "HelloThere",
        "caption": ""
    },
    "Funding TiddlyWiki": {
        "title": "Funding TiddlyWiki",
        "tags": "About HelloThere",
        "caption": ""
    },
    "Open Collective": {
        "title": "Open Collective",
        "tags": "About HelloThere",
        "caption": ""
    }
}

Obviously it would be easy to parameterize the filter used (here [tag[HelloThere]]. I’d have to think about how to do dynamic filters, or possibly use reduce, in order to change the list of fields to include (here title, tags, and caption.) But I’m sure it could be done.

But this is only for a simple, two-level structure. I have some ideas of how to make it simpler to build arbitrary JSON strings from simple parameters. I do it in JS all the time, and so I guess a JS operator might be enough. But I’m trying to figure out how to do it in wikitext.

Extract Tiddlers.tid (676 Bytes)

3 Likes

I think an $action-setjson widget would be an excellent solution, and would be more semantically transparent than further overtaxing $action-setfield.

Would it be prohibitively difficult to allow a path to be expressed via named subindexes as well as numerically? Maybe I’m misunderstanding, but I’d assume that using numeric parameters would require you to know the full structure of the JSON being manipulated in advance—which subindexes a given index has, and in what order. That seems a little inconsistent with the way the index-focused tools currently function: I don’t need to know anything about the current sequence of indexes in a data tiddler to use $action-setfield.

Thank you for your consideration!

My biggest concern is with arrays. I have useful techniques in JS to do this. But it distinguishes arrays from objects by the existence of integer parameters, which we really can’t do in TW. This seems to limit the utility a bit. But that’s only a bit. I think we could generally assume that by

[foo][0][bar]
[foo][1][baz]

the user means

{
  foo: ['bar', 'baz']
}

instead of

{
  foo: {
    '0': 'bar'
    '1': 'baz'
  }
}

If that’s acceptable, I could probably work on this. I’m leaving in a few days for a two-week vacation, so it wouldn’t be immediately.

The KeyValues plugin doesn’t handle these specific index writing/retrieval issues, but it’s a great tool for anyone working with data tiddlers, so thank you for mentioning it here, @Sunny!

For anyone who hasn’t encountered it before, @pmario’s KeyValues introduces a new filter operator that lets you search index keys and their values simultaneously; then, for any indexes that match on either side, it returns either the key, the value, or both. This is possible but much harder to do with vanilla TW operators, and would typically require multiple filter runs.

Honestly, I’d love to see something like keyvalues in the core as well… :thinking:

There has been a PR: Implement a data tiddler keyvalues operator by pmario · Pull Request #3971 · TiddlyWiki/TiddlyWiki5 · GitHub, which was rejected, because data-tiddlers should be discouraged.

There was a PR: Discourage / Deprecate data tiddlers by pmario · Pull Request #4389 · TiddlyWiki/TiddlyWiki5 · GitHub, which was rejected, because we are not there yet.

As the json-operators where introduced, I did have an idea, how I could make it easier to work with them in a similar way as the keyvalues-plugin. I did not immediately write down the mechanism I had in mind. – So I forgot it :frowning:

IMO data-tiddlers will go nowhere, since we have the json-operators

That’s a pity! I agree with you (and @Mohammad, as per that GH thread): data tiddlers are perfect for “unique” data. And I say this as a huge proponent of fields and templating! I work with a lot of data that absolutely does not need to be chopped up into tiddlers with all their associated metadata, and I can’t justify making a once-off field for every key-value pair I’d like to record. I can’t imagine any alternatives that wouldn’t be less efficient… even JSON tiddlers are more verbose when it comes to simple key-value pairs.


Edit: Here’s @jeremyruston’s (2019) position as per the first PR you linked:

The implementation of data tiddlers is spread throughout the core code, and hard to work with. I think that the whole idea was misguided: it was an attempt to avoid the proliferation of tiddlers, but I now think that that is a non-goal.

I hope this isn’t indicative of the current design goals. My largest wikis have upwards of 20,000 tiddlers — and the majority of those have some substantive text content, or at least more then 2-3 “user” fields. I’m always trying to cut down on total file size and increase filter efficiency, so keeping my tiddler count as small as feasible is a big priority for me. And similar, my field list is already so crowded that I tend to disable the editor dropdown.

If I were to convert all my data tiddlers to “full” tiddlers, it would bloat my wikis substantially. In fact, the experiments that inspired my OP were born out of a desire to avoid stub tiddlers!


In any case, thank you (to both @pmario and @jeremyruston) for continuing to support those of us who do use and love data tiddlers.

1 Like

Drifting OT here but;

I don’t understand why we don’t just use fields as a lightweight alternative to dictionary tiddlers. I mean, it is already an established concept in TW. A dictionary list looks pretty much like a bunch of custom fields. If a user understands custom fields, then “fields based data tiddlers” are almost self evident, what it is, how to use etc.

I’d guess fields could be “extended” to allow for nesting if needed (mytiddler!!fieldlevel1!!fieldlevel2!!...) and even visually they could be indented. This is way more “wikitexty” than having to use json.

I was actually dreaming (well, not literally) about this last night! It would really cut down on the the number of prefixes needed to establish a unique field name or to get fields to sort together… and indenting “child” fields would be a nice visual indicator.

It wouldn’t obviate my desire for data tiddlers, though. [fields[]count[]] = 753 in my largest wiki, and [all[shadows+tiddlers]fields[]count[]] gets up to 814. I wouldn’t want to further pollute that space with hundreds (if not thousands!) of single-use field names… indexes are appealing precisely because they don’t “count”, performance-wise, unless I explicitly choose to include them.

I wonder, though, if we could simply handle this at a view/edit level, with storage in JSON, to avoid adding new data types that TW needs to care about. Imagine an editor something like this: JSON Editor Online: edit JSON, format JSON, query JSON.

A good example of a practical use for DataDictionary tiddlers (type=application/x-tiddler-dictionary) is TiddlyTools/Settings/Colors/X11, which defines a mapping from X11 color names to #RRGGBB color values.

Similarly, in the TWCore, the palette tiddlers ($:/palettes/...) are DataDictionary tiddlers that define names used by the <<colour ...>> macro to apply CSS color values in $:/themes/tiddlywiki/vanilla/base and other theme definition tiddlers.

In both cases, the simple name:value format is much easier to read and edit than using JSON tiddlers (type=application/json) which have lots more punctuation (curly braces, quotes around text values, commas) and are much more prone to errors.

Having said that…

For most of my TiddlyTools configuration tiddlers ($:/config/TiddlyTools/...), I use JSON tiddlers that are managed via dedicated UI interfaces that use <$action-setfield $tiddler=... $index=... $value=.../> or <$edit-text tiddler=... index=.../> widgets so users rarely need to hand-edit or even view the underlying JSON configuration.

Still, in some cases, I’ve used fields to store the configuration settings, and in a few instances, I’ve even used a combination of both indexes AND fields in the same config tiddler.

-e