{"id":16980968,"url":"https://github.com/guzba/sunny","last_synced_at":"2025-03-22T15:30:37.127Z","repository":{"id":225589469,"uuid":"766357064","full_name":"guzba/sunny","owner":"guzba","description":"JSON in Nim with Go-like field tags.","archived":false,"fork":false,"pushed_at":"2024-11-09T17:21:24.000Z","size":235,"stargazers_count":15,"open_issues_count":5,"forks_count":1,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-03-18T12:40:15.758Z","etag":null,"topics":["json"],"latest_commit_sha":null,"homepage":"","language":"Nim","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/guzba.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","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}},"created_at":"2024-03-03T03:09:38.000Z","updated_at":"2025-03-11T06:11:46.000Z","dependencies_parsed_at":"2024-03-11T22:35:59.520Z","dependency_job_id":"8e4e3b2c-cb95-426f-9ca6-1a5427dd1a31","html_url":"https://github.com/guzba/sunny","commit_stats":{"total_commits":74,"total_committers":2,"mean_commits":37.0,"dds":"0.013513513513513487","last_synced_commit":"c2aa1e6809c09011d33bf75e54ac8499d23bac0d"},"previous_names":["guzba/sunny"],"tags_count":11,"template":false,"template_full_name":"treeform/nimtemplate","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/guzba%2Fsunny","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/guzba%2Fsunny/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/guzba%2Fsunny/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/guzba%2Fsunny/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/guzba","download_url":"https://codeload.github.com/guzba/sunny/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":244978361,"owners_count":20541840,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","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":["json"],"created_at":"2024-10-14T02:04:16.335Z","updated_at":"2025-03-22T15:30:36.650Z","avatar_url":"https://github.com/guzba.png","language":"Nim","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Sunny\n\n`nimble install sunny`\n\n[API reference](https://guzba.github.io/sunny/)\n\nSunny is fast JSON library for Nim that supports field tags like those found in Go. Field tags help make working with real-world JSON comfortable and easy.\n\n## The basics\n\nTo parse JSON into an instance, use `fromJson`:\n\n```nim\nimport sunny\n\ntype MyType = object\n  a: int\n  b: string\n\nlet instance = MyType.fromJson(\"\"\"{\"a\":3,\"b\":\"foo\"}\"\"\")\nassert instance.a == 3\nassert instance.b == \"foo\"\n```\n\nTo encode an instance to JSON, use `toJson`:\n```nim\nimport sunny\n\ntype MyType = object\n  a: int\n  b: string\n\nlet instance = MyType(a: 42, b: \"boo\")\necho instance.toJson() # \"\"\"{\"a\":42,\"b\":\"boo\"}\"\"\"\n```\n\n## Using field tags\n\nSunny supports field tags exactly like those found in Go. Field tags are a comma-separated list and the first tag is always used for optionally renaming a field.\n\nThe supported tags are currently `rename/skip`, `omitempty`, `required` and `string`.\n\n### Renaming fields\n\nOften the JSON you need to consume (or produce) will not use the style convention you wish it did. This makes being able to rename fields easily very helpful. The new name for a field is **always** the first tag.\n\nThis new name will be used by both `fromJson` and `toJson`.\n\n```nim\ntype Example = object\n  myField {.json: \"my_field\".}: int\n\nlet instance = Example.fromJson(\"\"\"{\"my_field\":9000}\"\"\")\nassert instance.myField == 9000\n\necho Example(myField: -1).toJson() # \"\"\"{\"my_field\":-1}\"\"\"\n```\n\n### Skipping fields\n\nAnother common situation is having some fields on an object that you never want to be included in the JSON output. A special tag of \"-\" indicates the field should be skipped.\n\nThis skips the field in both `fromJson` and `toJson`.\n\n```nim\ntype Example = object\n  myField {.json: \"-\".}: int\n\necho Example(myField: 42).toJson() # \"\"\"{}\"\"\"\n```\n\n### Omitting empty fields\n\nIn situations such as providing a JSON API for public consumption, you may want to omit including keys when they contain a default or empty value. This can avoid confusion around if there is difference between `\"key\":null` and `\"key\"` not being present.\n\nUsing the `omitempty` tag will result in `toJson` not including the field when encoding JSON if the field is empty, meaning the value is 0, an empty string, an empty seq, or an empty object.\n\n(Be mindful of the the leading `,` which leaves first tag empty indicating the field should not be renamed.)\n\n```nim\ntype Example = object\n  foo {.json: \",omitempty\".}: string\n\necho Example(foo: \"\").toJson() # \"\"\"{}\"\"\"\necho Example(foo: \"bar\").toJson() # \"\"\"{\"foo\":\"bar\"}\"\"\"\n```\n\n### Required fields\n\nWhile this tag does not exist in Go, Sunny supports the `required` tag.\n\nUsing this tag indicates that the field must be both present in the JSON and must not be `null`. If the field is missing or `null`, an exception is raised.\n\n```nim\ntype Example = object\n  x {.json: \",required\".}: int\n\n# Both of these raise an exception since `x` is tagged as a required field.\nlet instance = Example.fromJson(\"\"\"{}\"\"\")\nlet instance = Example.fromJson(\"\"\"{\"x\":null}\"\"\")\n\n# This works since `x` is present and non-`null`.\nlet instance = Example.fromJson(\"\"\"{\"x\":9000}\"\"\")\nassert instance.x == 9000\n```\n\n### The string field tag\n\nIt is quite common to find JSON APIs that encode numbers as strings. This is usually motivated by Javascript which has an interesting approach to numbers.\n\nIn Nim, you may want to parse a field as a number (integer or floating-point) even if it may be encoded as a string JSON. Using the `string` field tag makes this easy.\n\nThis field tag applies to both `fromJson` and `toJson`.\n\n```nim\ntype Example = object\n  x {.json: \",string\".}: int\n\nlet instance = Example.fromJson(\"\"\"{\"x\":\"42\"}\"\"\")\nassert instance.x == 42\n\necho Example(x: 42).toJson() # \"\"\"{\"x\":\"42\"}\"\"\"\n```\n\n### Using multiple field tags\n\nUsing multiple field tags is supported and easy to do since they are just a comma-separated list:\n\n```nim\ntype Example = object\n  myField {.json: \"my_field,omitempty,string\".}: int\n```\n\n## Custom `fromJson` and `toJson`\n\nWhile using field tags solves many of the most common problems when working with JSON, sometimes more control is needed.\n\nTaking inspiration from [jsony](https://github.com/treeform/jsony), Sunny supports calling custom `fromJson` and `toJson` hooks for types where you need more control than field tags provide.\n\nFor the example below lets imagine that `Example.data` holds binary data. Binary data does not mix with JSON since JSON must be UTF-8 encoded. By implementing custom `fromJson` and `toJson` procs, the binary data can be transparently base64 encoded/decoded making it perfectly safe for JSON.\n\n```nim\nimport sunny, std/base64\n\ntype Example = object\n  data: string\n\nproc fromJson*(v: var Example, value: JsonValue, input: string) =\n  # Call the default `fromJson` in `sunny` to do the initial parsing.\n  sunny.fromJson(v, value, input)\n  # Now overwrite `data` with the base64 decoded raw bytes.\n  v.data = base64.decode(v.data)\n\nproc toJson*(src: Example, s: var string) =\n  # Here we make a new temporary instance and assign `data` to be the\n  # base64 encoded string instead of the raw bytes.\n  var tmp: Example\n  tmp.data = base64.encode(src.data)\n  # Call the default `toJson` in `sunny` now that `data` is safely base64 encoded.\n  sunny.toJson(tmp, s)\n```\n\nTo implement behavior similar to [jsony](https://github.com/treeform/jsony)'s `newHook` and `postHook`, try something like this:\n\n```nim\nproc fromJson*(v: var Example, value: JsonValue, input: string) =\n  # Any code before `sunny.fromJson` is the equivalent of a `newHook`.\n\n  sunny.fromJson(v, value, input)\n\n  # Anything after `sunny.fromJson` is the equivalent of a `postHook`.\n```\n\nNote that you do not need to re-implement parsing just to have a custom hook, simply calling `sunny.fromJson` will take care of all the default behaviors including field tags.\n\n## Raw JSON\n\nSome JSON APIs use a form of variant object, where a `type` field will indicate what is stored in another field like `object`. By using `RawJson` you can indicate that a field should be treated as unparsed JSON which can then be parsed into a specific object type at a later time:\n\n```nim\ntype Container = object\n  `type`: string\n  `object`: RawJson\n\nlet a = Container.fromJson(\"\"\"{\"type\":\"event\",\"object\":{}}\"\"\")\n\ntype Event = object\n  # ...\n\nlet b = Event.fromJson(a.`object`)\n```\n\n## Default behaviors\n\nSunny's default behavior when parsing fields is loose / not strict.\n\n* Fields are not required (use `required` field tag to become stricter).\n* A missing field and `\"field\": null` are treated as the same thing.\n\nThis means you can easily parse the parts of a JSON blob you care about without a headache.\n\nWhile Sunny is loose about the presence / absence of fields, Sunny is strict about certain things to protect against unexpected bugs. These include:\n\n* Detecting duplicate keys when parsing JSON (raises an exception instead of last-key-wins or something odd like that).\n* Invalid UTF-8 will be detected and raise an exception (JSON must be valid UTF-8).\n* All JSON values are validated as part of parsing, avoiding frustrating \"parses-on-my-machine\" situations caused by things like \"10_000\" working in Nim's `parseInt` while not being valid JSON.\n\nIn addition to those protective measures, Sunny is also an iterative parser. This is very important when parsing untrusted inputs. A recursive parser attempting to parse an adversarial JSON blob can result in a stack overflow, terminating your process with zero information about what happened or why. This is not a great situation to find oneself in.\n\n## Performance\n\nSunny is a performance-aware library that includes some SIMD-optimized fast-paths. I'll include some benchmarks here later but you can expect significantly faster parsing and encoding than std/json. The performance is ~ the same as that of [jsony](https://github.com/treeform/jsony).\n\n## Testing\n\nTo prevent Sunny from causing a crash or otherwise misbehaving on bad JSON, a fuzzer has been run against it. You can run the fuzzer any time by running `nim c -r tests/fuzz.nim`\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fguzba%2Fsunny","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fguzba%2Fsunny","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fguzba%2Fsunny/lists"}