Working Group

#792 Haystack JSON Encoding

Gareth David Johnson Fri 28 Feb

This is a proposal for an additional JSON encoding format to the Haystack standard...

Haystack data has its own type system. It has these granular types…

  • String
  • Number (with units)
  • Bool
  • Uri
  • Ref
  • Def
  • Date
  • Time
  • Date time
  • Coord
  • XStr

It also includes these collection types…

  • List: an array
  • Dict: a map
  • Grid: a table

The default encoding used with these values is Zinc.

Here’s an example of a Zinc grid…

ver:"3.0" projName:"test"
dis dis:"Equip Name",equip,siteRef,installed
"RTU-1",M,@153c-699a "HQ",2005-06-01
"RTU-2",M,@153c-699a "HQ",1999-07-12

Zinc can also be encoded to a JSON format. For example…

{
  "meta": {"ver":"3.0", "projName":"test"},
  "cols":[
    {"name":"dis", "dis":"Equip Name"},
    {"name":"equip"},
    {"name":"siteRef"},
    {"name":"installed"}
  ],
  "rows":[
    {"dis":"RTU-1", "equip":"m:", "siteRef":"r:153c-699a HQ", "installed":"d:2005-06-01"},
    {"dis":"RTU-2", "equip":"m:", "siteRef":"r:153c-699a HQ", "installed":"d:999-07-12"}
  ]
}

Problems with Zinc

Web Developers are used to working with JSON. Both servers (Node) and browsers include native JSON parsing libraries. In order to to work with Zinc encoded data using the standard encoding, the browser’s JavaScript engine has to parse a lot of very large strings. Tests show that parsing JSON is faster than parsing large Zinc encoding strings in these environments.

Even the JSON version of Zinc still has a lot string parsing involved for the granular types. For example, in the above table a site ref is ‘r:153c-699a HA'. This string still has to be parsed to get the actual reference value. Therefore if we use standard or JSON encoding for Zinc, a client still has a lot of work involved in parsing strings.

As well as performance issues, there’s also a data accessibility issue. Since no primitive JSON values are being used, a consumer of any REST APIs has to have some form of client library. This prevents customers using a lot of the standard tools and libraries out there for working with this style of JSON. One example is using JSON schema to validate scalar values automatically.

Zinc always requires a sophisticated client library to parse and work with Haystack data. Not requiring a client library (or a far lighter one) would make it easier for Developers to work with Haystack data.

Hayson

A new encoding format for Haystack related data is required that’s JSON based. It’s nicked named Hayson but it should be noted this is just a JSON encoding schema.

The goal of this format is…

  • All types shouldn’t require additional parsing by a client beyond JSON.parse(…) where possible.
  • It should be simple for a developer or system integrator to read and work with.
  • Hayson should look just like standard JSON wherever it possibly can.
  • High fidelity: there is no loss of data in the encoding. For instance…
    • A Hayson dict can never be confused by a client with a Hayson grid.
    • A Haystack Ref should never be accidentally confused as a string by the string starting with @.
  • All JSON data is valid Haystack data.
    • A string is a haystack string, a boolean is a haystack boolean, an object a dict, an array a list etc.

Format

The encoding capitalizes on the extremely fast native JSON parsers all modern web browsers have.

Notes

Kind

Why use _kind below? An underscore is invalid as a tag name therefore making it a valid symbol to be used. The text for kind is directly mapped back to the Kind enumeration used in SkySpark.

A dict is the only object that doesn’t specify a kind. Therefore all JSON objects without a kind are dicts. This is very useful when a grid has to list its rows as dicts.

Value

The ‘val’ is the heart of the encoded value. It never requires further parsing to read the value.

Haystack Types

Each Haystack Type encoded as Hayson...

String

A JSON string.

"a string"

Number

A JSON number. If the number has units, it’s an object with both val and unit.

The kind can be determined via the use of ‘unit’.

123

// or if the number has 
// units...

{
  "_kind": "Num",
  "val": 123,
  "unit": "m"
}

// Handle infinity...
{
  "_kind": "Num",
  "val": "Inf"
}

Bool

A JSON boolean

true
// or
false

List

A JSON array.

[]

// A list with some values...
[ true, 123, "a string" ]

Dict

A JSON object.

If it’s an object and no kind is specified, it’s assumed to be a dict.

{}

// A dict with some values...
{
  "site": "A site",
  "num": 123,
  "bool": true
}

Grid

A grid object specifies a kind.

A column has a name and an optional meta dict.

Rows and columns are optional if the grid is empty.

Each row is encoded as a dict.

The version is governed by the the associated MIME type. Therefore versioning relies on standard HTTP content negotiation.

{
  "_kind": "Grid",
  "meta": { "foo": "bar" },
  "cols": [
    {
      "name: "id",
      "meta": { "size": 123 }   
    },
    {
      "name": "dis"
    }   
  ],
  "rows": [
    { "id": 1, "dis": "Hall" },
    { "id": 2, "dis": "Bedroom" }
  ]
}

If there's no column meta, the column names can be specified as an array of strings...

{
  "_kind": "Grid",
  "meta": { "foo": "bar" },
  "cols": [ "id", "dis" ],
  "rows": [
    { "id": 1, "dis": "Hall" },
    { "id": 2, "dis": "Bedroom" }
  ]
}

If there's no meta required then it can also be omitted...

{
  "_kind": "Grid",
  "cols": [ "id", "dis" ],
  "rows": [
    { "id": 1, "dis": "Hall" },
    { "id": 2, "dis": "Bedroom" }
  ]
}

Marker

A marker object.

{
  "_kind": "Marker"
}

Null

A JSON null.

null

Remove

A remove object.

{
  "_kind": "Remove"
}

NA --

A not available object.

{
  "_kind": "NA"
}

Ref

An object for a reference with optional display name.

Since the dis is optional, always specify the kind.

{
  "_kind": "Ref",
  "val": "/foo",
  "dis": "Links to foo"
}

Date

A date object.

{
  "_kind": "Date",
  "val": "2015-06-08"
}

Time

A time object.

{
  "_kind": "Time",
  "val": "15:47:41"
}

DateTime

An object with a date, time and timezone value.

The tz parameter is optional. It defaults to ‘GMT’.

The val is a standard ISO 8601 formatted date time.

{
  "_kind": "DateTime"
  "val": "2015-06-08T15:47:41-04:00",
  "tz": "New_York"
}

Uri

A URI object.

{
  "_kind": "Uri",
  "val": "https://j2inn.com"
}

Coord

A co-ordinate object.

{
  "_kind": "Coord",
  "lat": 51.019371,
  "lng": -0.453980
}

XStr

An XStr object.

{
  "_kind": "XStr",
  "type": "Type",
  "val": "value"
}

Example

Here's a simple grid of sites encoded using Hayson...

{
  "_kind": "Grid",
  "meta": {},
  "cols": [ "id", "area", "dis", "geoAddr", "geoCoord", "geoCountry", "geoPostalCode", 
    "geoState", "geoStreet", "hq", "metro", "occupiedEnd", "occupiedStart", 
    "primaryFunction", "regionRef", "site", "store", "storeNum", "tz", "weatherRef",
    "yearBuilt", "mod"
  ],
  "rows": [
    {
      "id": {
        "_kind": "Ref",
        "val": "p:demo:r:25aa2abd-c365ce5b",
        "dis": "Headquarters"
      },
      "area": {
        "_kind": "Number",
        "val": 140797,
        "unit": "ft²"
      },
      "dis": "Headquarters",
      "geoAddr": "600 W Main St, Richmond, VA",
      "geoCity": "Richmond",
      "geoCoord": {
        "_kind": "Coord",
        "lat": 37.545826,
        "lng": -77.449188
      },
      "geoCountry": "US",
      "geoPostalCode": "23220",
      "geoState": "VA",
      "geoStreet": "600 W Main St",
      "hq": {
        "_kind": "Marker"
      },
      "metro": "Richmond",
      "occupiedEnd": {
        "_kind": "Time",
        "val": "18:00:00"
      },
      "occupiedStart": {
        "_kind": "Time",
        "val": "08:00:00"
      },
      "primaryFunction": "Office",
      "regionRef": {
        "_kind": "Ref",
        "val": "p:demo:r:25aa2abd-5c556aba",
        "dis": "Richmond"
      },
      "site": {
        "_kind": "Marker"
      },
      "tz": "New_York",
      "weatherRef": {
        "_kind": "Ref",
        "val": "p:demo:r:25aa2abd-a02bf086",
        "dis": "Richmond, VA"
      },
      "yearBuilt": 1999,
      "mod": {
        "_kind": "DateTime",
        "val": "2020-01-09T18:17:34.232Z",
        "tz": "UTC"
      }
    },
    {
      "id": {
        "_kind": "Ref",
        "val": "p:demo:r:25aa2abd-96516c18",
        "dis": "Short Pump"
      },
      "area": {
        "_kind": "Number",
        "val": 17122,
        "unit": "ft²"
      },
      "dis": "Short Pump",
      "geoAddr": "11282 W Broad St, Richmond, VA",
      "geoCity": "Glen Allen",
      "geoCoord": {
        "_kind": "Coord",
        "lat": 37.650338,
        "lng": -77.606105
      },
      "geoCountry": "US",
      "geoPostalCode": "23060",
      "geoState": "VA",
      "geoStreet": "11282 W Broad St",
      "metro": "Richmond",
      "occupiedEnd": {
        "_kind": "Time",
        "val": "21:00:00"
      },
      "occupiedStart": {
        "_kind": "Time",
        "val": "10:00:00"
      },
      "primaryFunction": "Retail Store",
      "regionRef": {
        "_kind": "Ref",
        "val": "p:demo:r:25aa2abd-5c556aba",
        "dis": "Richmond"
      },
      "site": {
        "_kind": "Marker"
      },
      "store": {
        "_kind": "Marker"
      },
      "storeNum": 3,
      "tz": "New_York",
      "weatherRef": {
        "_kind": "Ref",
        "val": "p:demo:r:25aa2abd-a02bf086",
        "dis": "Richmond, VA"
      },
      "yearBuilt": 1999,
      "mod": {
        "_kind": "DateTime",
        "val": "2020-01-09T18:17:34.323Z",
        "tz": "UTC"
      }
    }
  ]
}

MIME Type

Hayson has its own MIME type that can be used in HTTP requests and responses for content negotiation…

application/vnd.haystack.v1+json

For example, when an HTTP request is made with the When this MIME type is specified, a server should respond with this data content with this content type.

Since Zinc has been around for a while, the standard application/json MIME type should return Zinc JSON encoded data.

Conclusion

Using Hayson instead of Zinc for all server and client communication provides the following benefits…

  • Less of a learning curve for Developers.
  • Uses a globally accepted encoding format.
  • Simple to understand.
  • No additional client libraries required to work with the data.
  • High performance. Tests show it’s between 10 and 5.5 times faster than parsing standard Zinc encoded data in a browser.

Steve Eynon Fri 28 Feb

Hi Gareth, this reads very well as I have my own gripes with Haystack encoded JSON; please forgive me for not knowing who you are but...

  • is this a proposal for a new standard
  • or are you a Project Haystack representative and this is a new standard?

Many thanks,

Steve. (Project Haystack User / a nobody)

Gareth David Johnson Fri 28 Feb

I'm proposing this as an addition to the Haystack standard.

I originally raised this with Brian. He mentioned creating a Working Group so we could input from across the industry.

If ratified, this would be an alternative to the existing JSON Zinc encoding and would be part of the Haystack standard.

Cheers, Gareth (Software Architect for J2/Siemens. Former Niagara Core Architect. Still considers himself a nobody).

Kevin Smith Fri 28 Feb

Good proposal. Can you provide additional data on your findings of the parsing performance tests that you have done?

Richard McElhinney Sat 29 Feb

Gareth..great work and I agree with your sentiment on developer adoption.

As an industry we have a long way to go in terms of breaking down the barriers for developer adoption and new additions to the standard like you are proposing are a large step in that direction.

So thank you for the time and effort you've taken to propose this.

Steve and Gareth...knowing each of you as I do..can I suggest this become a UK working group specialty.

Looking at Google Maps I think a meeting place in the Cotswolds would be a suitable mid-point location for you guys to get together and have a hackathon to come up with a reference implementation! ;)

andreas hennig Sat 29 Feb

YES !!!

when I implemented the ZINC-via-JSON format I rolled my eyes. This is finally a clean and pragmatic JSON.

The justification for the "_" sounds a bit rough. I am not familiar with JSON5, maybe the NAN / +infinity / -infinit problem could have been solved from there. But only if supported well in browsers and libraries.

Andreas

Gareth David Johnson Mon 2 Mar

Thank you everyone for your kind words. Richard you made me laugh out loud this morning.

In regards to Kevin's performance question, I have a TypeScript library that parses Zinc or Hayson. The Zinc decoding uses a classic recursive descent parser design. For each test, I created three different files in both formats. Obviously there's bias in the fact that I wrote the zinc parser.

  • sites.zinc (2 kb), sites.json (5 kb) - Contains the results of a simple site query.
  • defs.zinc (122 kb), defs.json (168 kb) - The defs database.
  • points.zinc (773 kb), points.json (1,298 kb) - Contains the results of a project wide query for all points.

The results are as follows...

*** Profile test read of sites.zinc: 8.57ms ***
*** Profile test read of sites.json: 0.865999ms ***
*** Profile test read of defs.zinc: 30.534799ms ***
*** Profile test read of defs.json: 7.4754ms ***
*** Profile test read of points.zinc: 79.7598ms ***
*** Profile test read of points.json: 58.1584ms ***

I ran the tests using NodeJS version 12.6.1. Chrome has similar results.

I believe the performance differences are down to the native JSON parsing these environments have.

Now you'll notice the Zinc files aren't as big. However if we gzip these files (like most web servers do)...

  • sites.zinc.gz (1 kb), sites.json.gz (1 kb)
  • defs.zinc.gz (32 kb), defs.json.gz (34 kb)
  • points.zinc.gz (64 kb), points.json.gz (87 kb)

When they're gzipped, I would argue there isn't really too much difference.

BTW, I'd appreciate it if people could also join this Working Group to give it a little bit more momentum in the community. Thanks!

Gareth David Johnson Mon 2 Mar

In regards to the underscore for kind. I use an underscore because it can never be the first letter of a tag in Haystack. Therefore a tag can't overwrite it accidentally.

Since a dict is a very commonly used type, it's the only object that doesn't require a _kind field. This has the added advantage of saving space when using it in a grid (which is basically an array of dicts++). It also means that any JSON object is a dict which is very powerful.

Gareth David Johnson Mon 2 Mar

A few other interesting things we can do with Hayson.

  • OpenAPI: we can have an OpenAPI document that can be used to provide support for Haystack based web services. The OpenAPI document provides documentation and data validation (since it can be used as JSON schema). This is exceptionally powerful. I can create this document and open source it accordingly. The tools for this are great.
  • YAML: I don't normally code JSON documents. I use YAML. YAML has everything Trio has (comments, multi-line string support) and more including great tool support. Here's my original example in YAML...
_kind: Grid
meta: {}
cols:
- name: id
- name: area
- name: dis
- name: geoAddr
- name: geoCity
- name: geoCoord
- name: geoCountry
- name: geoPostalCode
- name: geoState
- name: geoStreet
- name: hq
- name: metro
- name: occupiedEnd
- name: occupiedStart
- name: primaryFunction
- name: regionRef
- name: site
- name: store
- name: storeNum
- name: tz
- name: weatherRef
- name: yearBuilt
- name: mod
rows:
- id:
    _kind: Ref
    val: p:demo:r:25aa2abd-c365ce5b
    dis: Headquarters
  area:
    _kind: Number
    val: 140797
    unit: ft²
  dis: Headquarters
  geoAddr: 600 W Main St, Richmond, VA
  geoCity: Richmond
  geoCoord:
    _kind: Coord
    lat: 37.545826
    lng: -77.449188
  geoCountry: US
  geoPostalCode: '23220'
  geoState: VA
  geoStreet: 600 W Main St
  hq:
    _kind: Marker
  metro: Richmond
  occupiedEnd:
    _kind: Time
    val: '18:00:00'
  occupiedStart:
    _kind: Time
    val: '08:00:00'
  primaryFunction: Office
  regionRef:
    _kind: Ref
    val: p:demo:r:25aa2abd-5c556aba
    dis: Richmond
  site:
    _kind: Marker
  tz: New_York
  weatherRef:
    _kind: Ref
    val: p:demo:r:25aa2abd-a02bf086
    dis: Richmond, VA
  yearBuilt: 1999
  mod:
    _kind: DateTime
    val: '2020-01-09T18:17:34.232Z'
    tz: UTC
- id:
    _kind: Ref
    val: p:demo:r:25aa2abd-96516c18
    dis: Short Pump
  area:
    _kind: Number
    val: 17122
    unit: ft²
  dis: Short Pump
  geoAddr: 11282 W Broad St, Richmond, VA
  geoCity: Glen Allen
  geoCoord:
    _kind: Coord
    lat: 37.650338
    lng: -77.606105
  geoCountry: US
  geoPostalCode: '23060'
  geoState: VA
  geoStreet: 11282 W Broad St
  metro: Richmond
  occupiedEnd:
    _kind: Time
    val: '21:00:00'
  occupiedStart:
    _kind: Time
    val: '10:00:00'
  primaryFunction: Retail Store
  regionRef:
    _kind: Ref
    val: p:demo:r:25aa2abd-5c556aba
    dis: Richmond
  site:
    _kind: Marker
  store:
    _kind: Marker
  storeNum: 3
  tz: New_York
  weatherRef:
    _kind: Ref
    val: p:demo:r:25aa2abd-a02bf086
    dis: Richmond, VA
  yearBuilt: 1999
  mod:
    _kind: DateTime
    val: '2020-01-09T18:17:34.323Z'
    tz: UTC

Samuel Toh Tue 3 Mar

Hi, interesting work there. Always good to have options on the table.

Any chance to share with the community about your use-case which leads you into hayson?

Also, in your problem statement why are we comparing zinc with Hayson? I thought we should be looking at comparing the current zinc like JSON format vs Hayson?

I'm not sure how is hayson an extension to haystack's json format for now. Based on your proposal I think they will break existing JSON implementations as the current one requires the data type prefix to be in the data.

I think there are benefits to the existing JSON implementation even though the prefix can be seen as redundant at times. For instance fooBar vs s:foorBar. However, there are advantages here as well. Code can quickly identify what a data is based on its prefix, without blindly treating it as a piece of string. E.g. Dates and geo-coordinates.

I believe there are alternatives to boost the performance of the current JSON implementation. E.g. stripping away the whitespaces. If concern is due to bloated JSON payload, you can also look into HTTP compression options.

Gareth David Johnson Tue 3 Mar

My initial primary use case for Hayson was dealing with Haystack data when building web applications using React and Mobx as well as working with Haystack data in AWS. I've created a TypeScript library that parses Zinc, handles filters etc but even so I think there's a better way of handling this in general that doesn't require a client library as a dependency.

As stated, Hayson is an alternative JSON format. It has its own MIME Type so nothing will be broken.

The Zinc JSON encoding is still has a lot of the disadvantages of Zinc encoding whereby a client library is required to make sense out of the data. There's still a performance hit when parsing it regardless of white space. Integration with other libraries that expect already decoded JSON data is still an issue. Overall I think Hayson makes Haystack data more approachable for new developers outside of the Haystack community and provides better integration with modern tools and web services.

Samuel Toh Wed 4 Mar

Hayson is an alternative JSON format. It has its own MIME Type so nothing will be broken.

Sounds like if this is adopted then may have to maintain another standard. Looking at the bright side, I think you are saying JSON.parse(...) would be good enough for Javascript fans. Have you look into C# and Java? Do they have the equivalent and will just work?

There's still a performance hit.

I think typically for web apps the JSON we deal with won't be too huge in size. Like 100MB because even if we can parse them, they can have issues rendering it in modern browser. So if the JSON size is small then the performance benefit there can be negligible.

In general, I think Hayson is ok if we do not have the current JSON standard yet. One thing I kind of dislike it is the _kind key. Looks like not every {} object comes with it. The ones that do, Hayson seem to have gone from a simple r: prefix to be a Javascript object. Good thing about this kind of practise is that we can store a lot more meta data about the tag, which will be impossible to encode into a piece of string.

Steve Eynon Thu 5 Mar

For the benefit of others on the forum, here is a link to the existing Project Haystack specification for Zinc encoded JSON for comparison to this proposal.

The big take-away I see from Gareth's proposal is that once the JSON has been parsed into an object graph, all the string literals can be used as are. Unlike the current specification where the string values need to be parsed and de-constructed again to make sense of them.

It's also nice that numbers are stored as, well, numbers!

True, the new proposal is a lot more verbose than the current, but the extra meta is mostly constants so this both gzips very nicely and the string values easily intern'ed.

I don't mind the name _kind; it is common in many tech arenas to prefix system names with an underscore. For example, MongoDB do it with their _id primary key and I occasionally follow suit in code. I see the current one char prefix as a little cryptic so welcome the explicitness of a meta value.

The current standard is really only concerned with transporting Grids as an alternative to Zinc and (as far I can tell) it's nearly impossible to spot nested grids. The new syntax makes it easy to spot any data-type at any nested level.

In all to me, it looks like a much cleaner specification than the current. The big question is then, is it worth maintaining two standards? To that end, it would be really useful to know how many people actively use / have implemented the current JSON standard?

Two points I'd like make:

1. Infinite numeric values could be serialised like this:

{
  "_kind" : "Num",
  "val"   : "Inf"
}

It would mean parsers need to inspect the type of val (is it a number or is it a string?) but I think it is more acceptable than introducing a new explicit tag.

To address andreas hennig, because JSON5 is not natively implemented, and this format is largely for computers not humans, I would stick to pure JSON formats.

2. The format of the cols tag looks very superfluous. I understand the need of stating columns before we start adding rows, but could there not be a simple rule that states if an col element is a string, it is taken to be the name?

"cols": [ "id", "dis", "area", "geoAddr", "geoCoord", ... ]

Meta could still be used as usual, or mixed amongst the list:

"cols": [
  "id",
  "dis",
  {
    "name" : "area",
    "meta" : { "size" : 123 }
  },
  "geoAddr", "geoCoord", ... ]

In all I quite like this new Hayson / JZON (JSON Zinc Object Notation?) idea, so to recap my thoughts:

  1. Who uses the current Project Haystack JSON standard?
  2. Infinite numeric values (and other unseen edge cases) still need to be fleshed out
  3. A simple rule would allow Grid cols to be more succinct

Cheers,

Steve.

P.S. Gareth, I note your examples are all Javascript objects not JSON.

Josiah Johnston Thu 5 Mar

I quite like this concept.

Could _kind move from cell values into the column definitions?

Similarly, could units move into the column definitions?

I can't see any technical problems with moving _kind. units could present problems if different rows needed to use different units, which I don't expect to happen often in practice.

Best regards,

Josiah

Jason Briggs Thu 5 Mar

_kind couldn't go in the columns because each row could have a different kind. Also it happens all the time that unit are different per point.

Let's say I hit the read op, and got back all number points. Each point could have a different unit.

Gareth David Johnson Fri 6 Mar

Thanks for the feedback.

Steve...

I agree with your suggestions regarding infinity and columns. In practice, I imagine meta will be defined a lot of the time due to localisation of a column's display name. I also like the idea for handling infinity. I've also made the grid's meta optional if there's nothing in it.

I've also updated the code to all be JSON and not JavaScript. I had that highlighted in my original document but didn't add it to this post.

I've edited my original post to reflect these improvements.

Josiah Johnston Tue 10 Mar

Jason Briggs, What are some examples of different rows having different kinds?

Jason Briggs Tue 10 Mar

A point could be a Boolean or Number as an example. Here is the 1st 10 points I queried, you can see that they have different kinds, and units on each row.

curVal:false
kind:Bool
navName:Cool-2
---
curVal:25.311142478477333%
kind:Number
navName:OutsideDamper
unit:"%"
---
curVal:825.0391835106967kW
kind:Number
navName:kW
unit:kW
---
curVal:69.54355994698683°F
kind:Number
navName:ZoneTemp
unit:°F
---
curVal:12.701860966837174gal
kind:Number
navName:Consumption
unit:gal
---
curVal:63.25752247497029°F
kind:Number
navName:ReturnTemp
unit:°F
---
curVal:13.56478516137203%
kind:Number
navName:Heat
unit:"%"
---
curVal:false
kind:Bool
navName:Occupancy
---
curVal:770.2421831230303kWh
kind:Number
navName:kWh
unit:kWh
---
curVal:64°F
kind:Number
navName:Temp
unit:°F

Josiah Johnston Wed 11 Mar

Thanks Jason, very helpful!

I'd mostly been thinking of "tidy format" with one point per column rather than 1 point per row after looking at an example exported from SkySpark. Good to know that data can have this shape as well.

Normally, I'd work through a few diverse examples to validate a design proposal like Hayson, but I haven't found illustrative Haystack examples.

Verification of this proposal looks good since it's isomorphic to established formats.

-Josiah

Login or Signup to reply.