{"id":50968351,"url":"https://github.com/johanlindvall/lightning","last_synced_at":"2026-06-18T23:01:51.611Z","repository":{"id":361896230,"uuid":"1256324432","full_name":"JohanLindvall/lightning","owner":"JohanLindvall","description":"A fast Go JSON unmarshaler","archived":false,"fork":false,"pushed_at":"2026-06-16T20:28:11.000Z","size":345,"stargazers_count":1,"open_issues_count":1,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-16T21:20:39.426Z","etag":null,"topics":["go","json","simd"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/JohanLindvall.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-06-01T17:07:52.000Z","updated_at":"2026-06-16T18:28:09.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/JohanLindvall/lightning","commit_stats":null,"previous_names":["johanlindvall/lightning"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/JohanLindvall/lightning","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/JohanLindvall%2Flightning","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/JohanLindvall%2Flightning/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/JohanLindvall%2Flightning/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/JohanLindvall%2Flightning/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/JohanLindvall","download_url":"https://codeload.github.com/JohanLindvall/lightning/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/JohanLindvall%2Flightning/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34489229,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-18T02:00:06.871Z","response_time":128,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["go","json","simd"],"created_at":"2026-06-18T23:01:48.071Z","updated_at":"2026-06-18T23:01:51.605Z","avatar_url":"https://github.com/JohanLindvall.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Lightning ⚡\n\nA small Go code generator that emits fast, allocation-light\n`json.Unmarshaler` implementations from your struct definitions.\n\nInstead of decoding JSON with reflection at run time (like `encoding/json`),\nlightning reads a struct definition at build time and writes a hand-written\nstyle `UnmarshalJSON` method plus the recursive decoders it needs. The decoders\nshare a single set of scanning primitives in [`pkg/support`](pkg/support), so the\ngenerated files stay small.\n\n## Installation\n\nRun it straight from the module path (no clone needed):\n\n```sh\ngo run github.com/JohanLindvall/lightning@latest path/to/data.go\n```\n\n`@latest` can be any version, branch, or commit (`@v1.0.0`, `@main`, `@\u003csha\u003e`).\nOr install the binary once:\n\n```sh\ngo install github.com/JohanLindvall/lightning@latest\nlightning path/to/data.go\n```\n\nThe generated code imports `github.com/JohanLindvall/lightning/pkg/support`, so\nthe module you generate into must depend on lightning:\n\n```sh\ngo get github.com/JohanLindvall/lightning\n```\n\nA `go:generate` directive in the file that holds your structs works well:\n\n```go\n//go:generate go run github.com/JohanLindvall/lightning@latest $GOFILE\n```\n\n## How it works\n\nPoint the generator at a Go file containing one or more struct types. From\ninside this repo:\n\n```sh\ngo run . path/to/data.go\n```\n\n(`go run .` only works in this repo; elsewhere use the module path shown above.)\n\nFor each input file `FOO.go` it writes `FOO_unmarshal.go` next to it, containing\nan `UnmarshalJSON` method for every top-level struct type. The generated code\nimports `github.com/JohanLindvall/lightning/pkg/support` for the shared scanner.\n\nGiven:\n\n```go\npackage cloudflare\n\ntype Log struct {\n    RayID              string `json:\"RayID\"`\n    EdgeResponseStatus int64  `json:\"EdgeResponseStatus\"`\n    // ...\n}\n```\n\nyou get a `func (v *Log) UnmarshalJSON(data []byte) error` that parses the JSON\nwith an index-based scanner, no reflection, and no allocation on the common\npaths (unescaped strings, integers, object keys).\n\n## Supported types\n\n`string`, `bool`, every sized `int`/`uint` kind, `float32`/`float64`,\n`json.RawMessage` (and `RawValue`), `time.Time` (RFC 3339, like `encoding/json`;\nthe [`lax`](#the-lax-tag-option) option also accepts a space separator and Unix\ntimestamps), nested named and anonymous structs, slices, fixed-size arrays\n(`[N]T`), maps with string keys, pointers, and `interface{}`/`any` (decoded into\nthe usual Go representation of an arbitrary JSON value). Unknown object keys are\nskipped.\n\nA fixed-size array follows `encoding/json`: the leading elements are filled, a\nshorter JSON array leaves the remaining elements zero, and a longer one's extras\nare discarded.\n\nEmbedded struct fields are promoted like `encoding/json`: an embedded struct's\nexported fields decode as if they were the outer struct's own (an embedded\npointer is allocated on demand), a name present on both the outer struct and an\nembed is resolved by Go's shallower-wins rule, an equal-depth clash is dropped\nunless a single field is tagged, and an embedded field with its own JSON tag name\nis a plain named field rather than promoted. Embedding a type from another\npackage, whose fields aren't visible to the generator, is the one gap — it is\ndecoded as a single named field instead of being flattened.\n\n## The `nocopy` tag option\n\nBy default, string and `json.RawMessage` fields copy their bytes out of the\ninput, matching `encoding/json` semantics. Add `nocopy` to the json tag to make\na field alias the input buffer instead — zero-copy, but the caller must keep the\ninput `[]byte` unchanged while the result is in use:\n\n```go\ntype Log struct {\n    RayID string          `json:\"RayID,nocopy\"` // aliases the input\n    Body  json.RawMessage `json:\"Body,nocopy\"`  // aliases the input\n}\n```\n\n`nocopy` propagates through slices, maps, and pointers, but stops at struct\nboundaries (each struct's own field tags govern). Strings containing escape\nsequences still allocate, since they can't be a slice of the raw input.\n\n## Alternate field names\n\nA json tag may list several pipe-separated names. Any of them appearing in the\ninput fills the field, which is handy when an upstream source renamed a key and\nyou want to accept both spellings:\n\n```go\ntype Log struct {\n    EdgeResponseStatus int64 `json:\"EdgeResponseStatus|AnotherField\"`\n}\n```\n\nComma-separated options still follow the name as usual, so names and `nocopy`\ncombine freely — `json:\"Name|Title,nocopy\"` accepts both `Name` and `Title`,\nzero-copy.\n\n## The `lax` tag option\n\nBy default a value of the wrong type fails the whole decode: a string where a\nnumber is expected returns an error. Add `lax` to the json tag to make such a\nmismatch a no-op instead — the offending value is skipped and the field left at\nits zero value, while the rest of the object decodes normally:\n\n```go\ntype Log struct {\n    Status int64 `json:\"Status,lax\"` // a non-number Status is ignored, leaving 0\n}\n```\n\nOnly type mismatches are tolerated; genuinely malformed JSON (a syntax error in\nthe value) still fails, since a well-formed value of the wrong type can be\nskipped but a broken one cannot. `lax` works for every field type, including\nnested structs, slices, and maps, where a decode error anywhere in the value\nleaves the whole field unset. It combines with the other options and with\nalternate names — `json:\"Name|Title,nocopy,lax\"`.\n\nOn a `time.Time` field, `lax` additionally widens what counts as a valid\ntimestamp. Besides strict RFC 3339, it accepts a space in place of the `T`\ndate/time separator and a Unix timestamp given as a JSON number or numeric\nstring, inferring seconds, milliseconds, or microseconds from the magnitude; the\nresult is normalized to UTC. An unrecognized timestamp is skipped and the field\nleft unset, like any other lax mismatch. As with `nocopy`, the lenient parser\npropagates through slices, maps, and pointers (e.g. `[]time.Time`) but stops at\nstruct boundaries.\n\n## The `unwrap` tag option\n\nSome payloads carry a nested document as a *string* — JSON embedded in JSON,\nsometimes base64-encoded. Add `unwrap` to a field's json tag to decode through\nthat wrapper: the field's value is read as a JSON string, its body unescaped,\nand the result decoded as JSON into the field.\n\n```go\ntype Envelope struct {\n    Name    string  `json:\"name\"`\n    Payload Message `json:\"payload,unwrap\"` // value is a string holding JSON\n}\n```\n\nBoth forms are accepted automatically. If the unescaped string is itself JSON\n(its first non-whitespace byte starts a JSON value) it is decoded directly;\notherwise it is base64-decoded first (standard alphabet, with or without\npadding) and the decoded bytes are the JSON. So a `\"payload\"` of\n`\"{\\\"id\\\":7}\"` and of `\"eyJpZCI6N30=\"` both fill `Payload`. A `null` or empty\nstring leaves the field at its zero value.\n\nThe field decodes with its normal rules, so `unwrap` composes with the field's\ntype (struct, slice, map, scalar…) and with `nocopy` — a `nocopy` string inside\nthe embedded document aliases the decoded buffer, which is retained for as long\nas the result is in use. The embedded document is parsed as a fresh input, so\nits own whitespace, escaping, and structure are independent of the outer JSON.\n\n## Comment directives\n\nSome behavior is selected with a `//lightning:\u003cname\u003e` comment on the struct type\n(or its declaration), separate from the per-field json tags above.\n\n### `//lightning:compact`\n\nBy default a decoder calls `SkipWS` around every token so it accepts JSON with\nany whitespace. Mark a type `//lightning:compact` to assert the input has no\nwhitespace *between* tokens — the form `encoding/json`'s `Marshal` and most wire\nprotocols emit — and the generator drops those inter-token `SkipWS` calls,\ndecoding tokens back-to-back:\n\n```go\n//lightning:compact\ntype Log struct {\n    RayID  string `json:\"RayID\"`\n    Status int64  `json:\"Status\"`\n}\n```\n\nThis runs a few percent faster on object-heavy payloads (the `cloudflare-compact`\nbenchmark beats `cloudflare-nocopy`, its non-compact equivalent, by ~4%).\nWhitespace surrounding the whole document is still tolerated — a\ntrailing newline is fine — so only *inter-token* whitespace is assumed absent.\n\nThe directive is an assertion you make about the input: a compact decoder fed\ninput that does contain inter-token whitespace (for example the same document\npretty-printed) returns an error instead of parsing it. Use it only for sources\nthat are guaranteed compact. The directive applies to the whole type graph it\nroots, including nested structs, slices, and maps.\n\n## Generated function names\n\nThe `UnmarshalJSON` methods keep their exact name (the `json.Unmarshaler`\ninterface requires it). The unexported decoder helpers they call are named\n`lightning\u003cImportPath\u003e\u003cType\u003edecode…` — a prefix derived from the package's import\npath and the top-level type — so generating decoders for several types into one\npackage never produces colliding helper names. No annotation is needed; the\nprefix is automatic.\n\n## Key lookups\n\nWhen you only need a few values out of a document and don't want to generate (or\ndecode into) a struct, the [`pkg/json`](pkg/json) package exposes the scanner's\nkey-lookup primitives. They walk the input with the same allocation-free\n`Skip`/`ReadKey` machinery the generated decoders use, and every value they\nreturn aliases the input `[]byte` (keep it unchanged while the result is in\nuse). A returned value follows the same conventions throughout: a string keeps\nits surrounding quotes with escapes intact, an object or array spans the whole\n`{`…`}` or `[`…`]`, and a scalar is the literal token.\n\n- `Get(data []byte, keys ...string) ([]byte, int, error)` — walks the object-key\n  path `keys` one level per key and returns the value's raw bytes (and the offset\n  it starts at), without reporting a value type. With no keys it returns the whole\n  root value; a missing key returns `ErrKeyNotFound`.\n- `GetMany(data []byte, keys []string, out [][]byte) ([][]byte, error)` — looks up\n  several *top-level* keys in a **single pass** over the object, where N separate\n  `Get` calls would rescan it N times. Results are written into `out[:0]` (pass a\n  slice to reuse across calls, allocation-free; a `nil` reuses nothing) and\n  returned with `len == len(keys)`: `out[n]` is the value for `keys[n]`, or `nil`\n  if that key is absent. A missing key is reported by the `nil` slot, not an\n  error (a present key whose value is JSON `null` yields the bytes `\"null\"`,\n  distinct from absent); a non-object root or malformed JSON returns an error.\n- `ObjectEach(data []byte, fn func(key string, value []byte) error, keys ...string) error`\n  — calls `fn` for every member of the object reached by the path `keys` (the\n  root object with no keys). If `fn` returns an error, iteration stops and\n  returns it.\n\n```go\n// Pull a few fields out of a log record in one pass, reusing a scratch slice.\nkeys := []string{\"ClientIP\", \"EdgeResponseStatus\", \"RayID\"}\nvals, err := json.GetMany(data, keys, scratch[:0])\n// vals[0] == []byte(`\"203.0.113.23\"`), vals[1] == []byte(\"599\"), …\n```\n\nEach function has a **compact counterpart** — `GetCompact`, `GetManyCompact`,\n`ObjectEachCompact` — with the identical signature and result. Like the\n[`//lightning:compact`](#lightningcompact) directive, they assume the input has\nno whitespace *between* tokens (the form `encoding/json`'s `Marshal` and most\nwire protocols emit) and skip the inter-token `SkipWS` scans, running about 10%\nfaster; whitespace surrounding the whole document is still tolerated. Feed one\ninput that does contain inter-token whitespace and it may return an error, so use\nthem only for sources guaranteed compact.\n\n## String escaping and unescaping\n\nThe [`pkg/json`](pkg/json) package exposes the scanner's string codec on its\nown, for when you have a JSON string body (the bytes between the quotes) and\njust want to decode or encode it.\n\n**Unescaping** (escaped body → decoded value):\n\n- `UnescapeString(in []byte) (string, error)` — decodes the escapes in `in`. If\n  `in` contains no escapes the result aliases `in` with no copy; otherwise a new\n  string is allocated. `in` is left unchanged.\n- `UnescapeStringInto(in, out []byte) (string, error)` — same, but writes the\n  decoded bytes into `out` instead of allocating.\n  With no escapes the result aliases `in`; otherwise it aliases `out` and\n  allocates nothing when `cap(out) \u003e= len(in)`, since unescaping never lengthens\n  a string. Pass `out == in` (e.g. `in[:0]`) to decode truly in place,\n  overwriting `in`.\n\nBoth return a string that aliases a buffer, so keep that buffer unchanged while\nthe result is in use.\n\n**Escaping** (raw value → escaped body, escaping `\"`, `\\`, and control bytes;\n`/` is left as-is and `\\b`/`\\f` are written in `\\u00XX` form):\n\n- `EscapeString(s []byte, out *strings.Builder)` — writes the escaped form of\n  `s` to `out`. The common no-escape case is detected with a vectorized scan and\n  written straight to the builder, with no scratch buffer or copy.\n- `EscapeStringInto(s, out []byte) []byte` — appends the escaped form of `s` to\n  `out` and returns the extended slice, allocating nothing when `out` has room\n  (escaping can grow a string up to 6× for control bytes). Pass `out[:0]` to\n  reuse a buffer across calls.\n\nClean runs are skipped eight bytes at a time (SWAR), so strings that need little\nor no escaping cost roughly one `memcpy`.\n\n## Number parsing\n\nThe [`pkg/json`](pkg/json) package also exposes the scanner's float parser:\n\n- `ParseFloat(b []byte) (float64, error)` — parses the JSON number in `b` as a\n  `float64`. It takes the scanner's Clinger fast path first — when the mantissa is\n  exact and the decimal exponent is small, the result is a single multiply or\n  divide by a power of ten. Numbers that miss it (a mantissa ≥ 2^53 or a larger\n  exponent, e.g. high-precision coordinates) take an Eisel-Lemire pass that\n  converts the extracted mantissa and exponent with a 128-bit multiply, the same\n  fast path `strconv.ParseFloat` uses internally but without re-scanning the\n  digits; only the rare ambiguous or \u003e19-digit cases fall back to\n  `strconv.ParseFloat`. The Eisel-Lemire result is bit-for-bit identical to\n  `strconv` (verified by a differential fuzz test). `b` must be exactly one number\n  with no surrounding whitespace; trailing bytes or an empty input return an\n  error. Nothing is retained or copied.\n\n## Stripping default fields\n\nThe [`pkg/json`](pkg/json) package can also prune a JSON document in a single\npass, dropping object members whose value is a \"default\" — useful for shrinking\nverbose, mostly-default records (Cloudflare HTTP logs, say) before storing or\nforwarding them:\n\n- `StripDefaults(input, output []byte, defaults, keep [][]byte, ws WhitespaceMode) []byte`\n  — copies `input` to `output`, dropping every object member whose value is a\n  default and then dropping any object or array that this leaves empty. A value\n  is a default when it is byte-equal to one of `defaults`, compared against the\n  bare token — the unquoted contents for a string, the literal token for a number\n  or keyword (e.g. `[]byte(\"none\")`, `[]byte(\"false\")`). Empty values are *not*\n  special-cased: to drop empty strings (and other empty tokens) include an empty\n  entry `[]byte(\"\")` in `defaults`. A member is kept despite a default value when\n  its unquoted key is byte-equal to one of `keep` (e.g. `[]byte(\"WallTimeMs\")`).\n  String values keep their surrounding quotes and escapes; scalars keep their\n  literal token. (Empty `{}`/`[]` are always dropped, independent of `defaults`.)\n\n`output` is filled from the front and the populated prefix is returned; `input`\nis never modified. StripDefaults never lengthens the document, so `output` is grown\n(allocated) only when `cap(output) \u003c len(input)` — pass `input[:0]` to strip in\nplace, or a reused buffer to run allocation-free. It is best effort and copies\nmalformed input through unchanged.\n\n`ws` chooses how inter-token whitespace is handled:\n- `RemoveWhitespace` (the zero value) — tolerate any whitespace; output is compact.\n- `AssumeCompact` — assert the input has no inter-token whitespace and skip the\n  `SkipWS` scans (faster, as [`GetCompact`](#key-lookups) does); misreads spaced input.\n- `PreserveWhitespace` — keep the input's whitespace around surviving content, so a\n  pretty-printed document stays pretty-printed; only dropped members are removed.\n\n```go\n// \"\" opts empty strings in; \"0\"/\"none\"/\"false\"/\"unknown\" are the non-empty defaults.\ndefaults := [][]byte{[]byte(\"\"), []byte(\"0\"), []byte(\"none\"), []byte(\"false\"), []byte(\"unknown\")}\nkeep := [][]byte{[]byte(\"WallTimeMs\")} // retained even when their value is a default\nvar scratch []byte\nscratch = json.StripDefaults(record, scratch[:0], defaults, keep, json.AssumeCompact)\n// {\"a\":0,\"b\":\"x\",\"e\":\"\",\"WallTimeMs\":0}  -\u003e  {\"b\":\"x\",\"WallTimeMs\":0}\n```\n\nMatching is length-pre-filtered so a value or key longer than any candidate\nskips the scan, and a kept member is moved with a single `copy` when its\n`\"key\":value` span is contiguous in the input.\n\n## SIMD scanning\n\nTwo hot scan loops use a single vectorized pass instead of byte-at-a-time work,\nwith kernels in `pkg/support/index_amd64.s` and `pkg/support/index_arm64.s`\n(arm64 uses NEON/ASIMD, 16 bytes/pass, for both):\n\n- **next `\"` or `\\` in a string** — replaces two `bytes.IndexByte` scans; speeds\n  up string-heavy payloads. On amd64 it uses SSE2 (16-byte vectors, two compares\n  per 32-byte step), which avoids the `VZEROUPPER` an AVX2 routine must run on\n  every call — pure overhead for the short keys and values typical of JSON.\n- **next structural byte (`{`, `}`, `[`, `]`, `\"`)** — AVX2 on amd64, 32\n  bytes/pass, lets `skipObject` / `skipArray` jump over inert content (numbers,\n  keys, whitespace) when skipping unknown values. Skipping a large ignored\n  array/object is dramatically faster (the `skip-heavy` benchmark decodes at\n  \u003e50 GB/s, ~230× `encoding/json`).\n\nFeature detection is at run time (`golang.org/x/sys/cpu`); other platforms, CPUs\nwithout the feature, and inputs shorter than the vector width fall back to scalar\n(`bytes.IndexByte`-based) code. Behavior is identical across paths — verified by\nfuzzing each primitive against a reference and by `SkipValue`/decode round-trips\nvs `encoding/json`, on amd64 and on arm64 under qemu.\n\n## Benchmarks\n\nThe [`bench/`](bench) directory is a separate module (so its benchmark-only\ndependencies on [easyjson](https://github.com/mailru/easyjson) and\n[sonic](https://github.com/bytedance/sonic) stay out of the main module). It\nbenchmarks the same payload decoded four ways — lightning, `encoding/json`,\neasyjson, and bytedance/sonic — across each `bench/\u003ccase\u003e/` folder.\n\n**See the per-architecture results for the full, current numbers:\n[`bench/results_amd64.md`](bench/results_amd64.md) and\n[`bench/results_arm64.md`](bench/results_arm64.md).**\n\nRun them yourself with:\n\n```sh\n./bench/run_bench.sh\n```\n\nwhich (re)generates each case's deserializers and writes `bench/results.txt`\nand an architecture-specific `bench/results_\u003cgoarch\u003e.md` (so runs on different\nCPUs do not overwrite each other's committed results).\n\nRepresentative numbers for a 1.8 KB Cloudflare log (Go 1.26, amd64):\n\n| Decoder | ns/op | B/op | allocs/op | vs stdlib |\n|---|--:|--:|--:|--:|\n| lightning (`nocopy`) | ~660 | 0 | 0 | ~13× |\n| lightning (default)  | ~800 | 144 | 10 | ~10× |\n| easyjson             | ~1600–1770 | 24–144 | 1–10 | ~5× |\n| sonic                | ~4600 | 3380 | 40 | ~1.9× |\n| `encoding/json`      | ~8250 | 920 | 17 | 1.0× |\n\n## Layout\n\n| Path | Description |\n|---|---|\n| [`main.go`](main.go) | the generator (`package main`) |\n| [`pkg/support`](pkg/support) | shared JSON scanning primitives used by generated code |\n| [`pkg/json`](pkg/json) | small public API over the scanner (`Get`/`GetMany`/`ObjectEach`, `UnescapeString`, `ParseFloat`, `StripDefaults`) |\n| [`bench/`](bench) | benchmark module: hand-written `data.go` + `input.json` per case, plus the generated decoders, harness, and results |\n\nGenerated files (`*_unmarshal.go`, `bench/*/bench_test.go`, `bench/*/ej/`, and\nthe `bench/results.*` outputs) are reproducible and excluded from version\ncontrol via [`.gitignore`](.gitignore).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjohanlindvall%2Flightning","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjohanlindvall%2Flightning","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjohanlindvall%2Flightning/lists"}