{"id":19211016,"url":"https://github.com/bowbahdoe/json","last_synced_at":"2025-11-06T22:03:21.306Z","repository":{"id":62863408,"uuid":"536418112","full_name":"bowbahdoe/json","owner":"bowbahdoe","description":"A Java JSON Library intended to be easy to learn and simple to teach","archived":false,"fork":false,"pushed_at":"2024-11-21T01:52:04.000Z","size":1038,"stargazers_count":38,"open_issues_count":7,"forks_count":5,"subscribers_count":4,"default_branch":"main","last_synced_at":"2025-04-09T20:04:27.532Z","etag":null,"topics":["education","java","json","modern"],"latest_commit_sha":null,"homepage":"","language":"Java","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/bowbahdoe.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":"2022-09-14T04:52:29.000Z","updated_at":"2025-03-29T20:51:39.000Z","dependencies_parsed_at":"2024-11-09T13:40:40.266Z","dependency_job_id":"ad6c58ea-0348-401e-ac9b-8b079871211a","html_url":"https://github.com/bowbahdoe/json","commit_stats":null,"previous_names":[],"tags_count":24,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bowbahdoe%2Fjson","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bowbahdoe%2Fjson/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bowbahdoe%2Fjson/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bowbahdoe%2Fjson/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/bowbahdoe","download_url":"https://codeload.github.com/bowbahdoe/json/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248103865,"owners_count":21048245,"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":["education","java","json","modern"],"created_at":"2024-11-09T13:40:28.476Z","updated_at":"2025-11-06T22:03:16.265Z","avatar_url":"https://github.com/bowbahdoe.png","language":"Java","funding_links":[],"categories":[],"sub_categories":[],"readme":"# json\n\n[![javadoc](https://javadoc.io/badge2/dev.mccue/json/javadoc.svg)](https://javadoc.io/doc/dev.mccue/json)\n[![tests](https://github.com/bowbahdoe/json/actions/workflows/test.yml/badge.svg)](https://github.com/bowbahdoe/json/actions/workflows/test.yml)\n\u003cimg src=\"./bopbop.png\"\u003e\u003c/img\u003e\n\nA Java JSON Library intended to be easy to learn and simple to teach.\n\nRequires Java 21+.\n\n## Dependency Information\n\n### Maven\n\n```xml\n\u003cdependency\u003e\n    \u003cgroupId\u003edev.mccue\u003c/groupId\u003e\n    \u003cartifactId\u003ejson\u003c/artifactId\u003e\n    \u003cversion\u003e2024.11.20\u003c/version\u003e\n\u003c/dependency\u003e\n```\n\n### Gradle\n\n```\ndependencies {\n    implementation(\"dev.mccue:json:2024.11.20\")\n}\n```\n\n## What this does\n\nThe primary goals of this library are\n1. Be easy to learn and simple to teach.\n2. Have an API for decoding that is reasonably declarative and gives good feedback\n   on unexpected input.\n3. Make use of modern Java features.\n\nThe non-goals of this library are\n\n1. Provide an API for data-binding.\n2. Support every extension to the JSON spec.\n3. Handle documents which cannot fit into memory.\n\n## Tutorial\n\n\u003cdetails\u003e\n    \u003csummary\u003eShow\u003c/summary\u003e\n\n### The Data Model\n\nJSON is a data format. It looks like the following sample.\n\n```json\n{\n    \"name\": \"kermit\",\n    \"wife\": null,\n    \"girlfriend\": \"Ms. Piggy\",\n    \"age\": 22,\n    \"children\": [\n        {\n            \"species\": \"frog\",\n            \"gender\": \"male\"\n        },\n        {\n            \"species\": \"pig\",\n            \"gender\": \"female\"\n        }\n    ],\n    \"commitmentIssues\": true\n}\n```\n\nIn JSON you represent data using a combination of objects (maps from strings to JSON),\narrays (ordered sequences of JSON), strings, numbers, true, false, and null.\n\nTherefore, one \"natural\" way to think about the data stored in a JSON document\nis as the union of those possibilities.\n\n```\nJSON is one of\n- a map of string to JSON\n- a list of JSON\n- a string\n- a number\n- true\n- false\n- null\n```\n\nThe way to represent this in Java is using a sealed interface, which\nprovides an explicit list of types which are allowed to implement it.\n\n```java \npublic sealed interface Json\n        permits \n            JsonObject,\n            JsonArray,\n            JsonString,\n            JsonNumber,\n            JsonBoolean,\n            JsonNull {\n}\n```\n\nThis means that if you have a field or variable which has the type `Json`, you know\nthat it is either a `JsonObject`, `JsonArray`, `JsonString`, `JsonNumber`, `JsonBoolean`,\nor `JsonNull`.\n\nThat is the first thing provided by my library. There is a `Json` type\nand subtypes representing those different cases.\n\n```java\nimport dev.mccue.json.*;\n\npublic class Main {\n    static Json greeting() {\n        return JsonString.of(\"hello\");\n    }\n    \n    public static void main(String[] args) {\n        Json json = greeting();\n        switch (json) {\n            case JsonObject object -\u003e\n                    System.out.println(\"An object\");\n            case JsonArray array -\u003e\n                    System.out.println(\"An array\");\n            case JsonString str -\u003e\n                    System.out.println(\"A string\");\n            case JsonNumber number -\u003e\n                    System.out.println(\"A number\");\n            case JsonBoolean bool -\u003e\n                    System.out.println(\"A boolean\");\n            case JsonNull __ -\u003e\n                    System.out.println(\"A json null\");\n        }\n    }\n}\n```\n\nYou can create instances\nof these subtypes using factory methods on the types themselves.\n\n```java\nimport dev.mccue.json.*;\n\nimport java.util.List;\nimport java.util.Map;\n\npublic class Main {\n    public static void main(String[] args) {\n        JsonObject kermit = JsonObject.of(Map.of(\n                \"name\", JsonString.of(\"kermit\"),\n                \"age\", JsonNumber.of(22),\n                \"commitmentIssues\", JsonBoolean.of(true),\n                \"wife\", JsonNull.instance(),\n                \"children\", JsonArray.of(List.of(\n                        JsonString.of(\"Tiny Tim\")\n                ))\n        ));\n\n        System.out.println(kermit);\n    }\n}\n```\n\nOr by using factory methods on `Json`, which aren't guaranteed to give you\nany specific subtype but in exchange will handle converting any stray `null`s to `JsonNull`.\n\n```java\nimport dev.mccue.json.*;\n\nimport java.util.List;\nimport java.util.Map;\n\npublic class Main {\n    public static void main(String[] args) {\n        Json kermit = Json.of(Map.of(\n                \"name\", Json.of(\"kermit\"),\n                \"age\", Json.of(22),\n                \"commitmentIssues\", Json.of(true),\n                \"wife\", Json.ofNull(),\n                \"children\", Json.of(List.of(\n                        JsonString.of(\"Tiny Tim\")\n                ))\n        ));\n\n        System.out.println(kermit);\n    }\n}\n```\n\nFor `JsonObject` and `JsonArray`, there also use builders available which\ncan make it so that you don't need to write `Json.of` on every value.\n\n```java\nimport dev.mccue.json.Json;\n\npublic class Main {\n    public static void main(String[] args) {\n        Json kermit = Json.objectBuilder()\n                .put(\"name\", \"kermit\")\n                .put(\"age\", 22)\n                .putTrue(\"commitmentIssues\")\n                .putNull(\"wife\")\n                .put(\"children\", Json.arrayBuilder()\n                        .add(\"Tiny Tim\"))\n                .build();\n\n        System.out.println(kermit);\n    }\n}\n```\n\n### Writing\n\nOnce you have some `Json` you can write it out to a `String` using `Json.writeString`\n\n```java\nimport dev.mccue.json.Json;\n\npublic class Main {\n    public static void main(String[] args) {\n        Json songJson = Json.objectBuilder()\n                .put(\"title\", \"Rainbow Connection\")\n                .put(\"year\", 1979)\n                .build();\n\n        String song = Json.writeString(songJson);\n        System.out.println(song);\n    }\n}\n```\n\n```json\n{\"title\":\"Rainbow Connection\",\"year\":1979}\n```\n\nIf output is meant to be consumed by humans then whitespace can be added\nusing a customized instance of `JsonWriteOptions`.\n\n```java\nimport dev.mccue.json.Json;\nimport dev.mccue.json.JsonWriteOptions;\n\npublic class Main {\n    public static void main(String[] args) {\n        Json songJson = Json.objectBuilder()\n                .put(\"title\", \"Rainbow Connection\")\n                .put(\"year\", 1979)\n                .build();\n\n        String song = Json.writeString(\n                songJson,\n                new JsonWriteOptions()\n                        .withIndentation(4)\n        );\n        \n        System.out.println(song);\n    }\n}\n```\n\n```json\n{\n    \"title\": \"Rainbow Connection\",\n    \"year\": 1979\n}\n```\n\nIf you want to write JSON to something other than a `String`, you need to\nobtain a `Writer` and use `Json.write`.\n\n```java\nimport dev.mccue.json.Json;\n\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\n\npublic class Main {\n    public static void main(String[] args) throws IOException {\n        Json songJson = Json.objectBuilder()\n                .put(\"title\", \"Rainbow Connection\")\n                .put(\"year\", 1979)\n                .build();\n\n\n        try (var fileWriter = Files.newBufferedWriter(\n                Path.of(\"song.json\"))\n        ) {\n            Json.write(songJson, fileWriter);\n        }\n    }\n}\n```\n\n### Encoding\n\nTo turn a class you have defined into JSON, you just need to make a method\nwhich creates an instance of `Json` from the data stored in your class.\n\n```java\nimport dev.mccue.json.Json;\n\nrecord Muppet(String name) {\n    Json toJson() {\n        return Json.objectBuilder()\n                .put(\"name\", name)\n                .build();\n    }\n}\n\npublic class Main {\n    public static void main(String[] args) {\n        var beaker = new Muppet(\"beaker\");\n        Json beakerJson = beaker.toJson();\n\n        System.out.println(Json.writeString(beakerJson));\n    }\n}\n```\n\nThis process is \"encoding.\" You \"encode\" your data into JSON and then \"write\"\nthat JSON to some output.\n\nFor classes that you did not define, the logic for the conversion just needs to live somewhere.\nDealer's choice where, but static methods are generally a good call.\n\n```java\nimport dev.mccue.json.Json;\n\nimport java.time.Month;\nimport java.time.MonthDay;\nimport java.time.format.DateTimeFormatter;\n\nfinal class TimeEncoders {\n    private TimeEncoders() {}\n\n    static Json monthDayToJson(MonthDay monthDay) {\n        return Json.of(\n                DateTimeFormatter.ofPattern(\"MM-dd\")\n                        .format(monthDay)\n        );\n    }\n}\n\nrecord Muppet(String name, MonthDay birthday) {\n    Json toJson() {\n        return Json.objectBuilder()\n                .put(\"name\", name)\n                .put(\n                        \"birthday\", \n                        TimeEncoders.monthDayToJson(birthday)\n                )\n                .build();\n    }\n}\n\npublic class Main {\n    public static void main(String[] args) {\n        var elmo = new Muppet(\n                \"Elmo\",\n                MonthDay.of(Month.FEBRUARY, 3)\n        );\n        Json elmoJson = elmo.toJson();\n\n        System.out.println(Json.writeString(elmoJson));\n    }\n}\n```\n\n```json\n{\"name\":\"Elmo\",\"birthday\":\"02-03\"}\n```\n\nIf a class you define has a JSON representation that could be considered \"canonical\", the interface `JsonEncodable`\ncan be implemented. This will let you pass an instance of the class directly to `Json.writeString` or `Json.write`.\n\n```java\nimport dev.mccue.json.Json;\nimport dev.mccue.json.JsonEncodable;\n\nrecord Muppet(String name, boolean great)\n        implements JsonEncodable {\n    @Override\n    public Json toJson() {\n        return Json.objectBuilder()\n                .put(\"name\", name)\n                .put(\"great\", great)\n                .build();\n    }\n}\n\npublic class Main {\n    public static void main(String[] args) {\n        var gonzo = new Muppet(\"Gonzo\", true);\n        System.out.println(Json.writeString(gonzo));\n    }\n}\n```\n\n### Reading\n\nThe inverse of writing JSON is reading it.\n\nIf you have some JSON stored in a `String` you can\nread it into `Json` using `Json.readString`.\n\n```java\nimport dev.mccue.json.Json;\n\npublic class Main {\n    public static void main(String[] args) {\n        Json movie = Json.readString(\"\"\"\n                {\n                    \"title\": \"Treasure Island\",\n                    \"cast\": [\n                        {\n                            \"name\": \"Kermit\",\n                            \"role\": \"The Captain\",\n                            \"muppet\": true\n                        },\n                        {\n                            \"name\": \"Tim Curry\",\n                            \"role\": \"Long John Silver\",\n                            \"muppet\": false\n                        }\n                    ]\n                \n                }\n                \"\"\");\n\n        System.out.println(movie);\n    }\n}\n```\n\nIf that JSON is coming from another source, you need to obtain a `Reader` and use `Json.read`.\n\n```java\nimport dev.mccue.json.Json;\n\nimport java.io.IOException;\nimport java.io.Reader;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\n\npublic class Main {\n    public static void main(String[] args) throws IOException {\n        // If you were following along, we created this earlier!\n        Json song;\n        try (Reader fileReader = Files.newBufferedReader(\n                Path.of(\"song.json\"))\n        ) {\n            song = Json.read(fileReader);\n        }\n\n        System.out.println(song);\n    }\n}\n```\n\nIf the JSON you provide is malformed in some way, a `JsonReadException` will be thrown.\n\n```java\nimport dev.mccue.json.Json;\n\npublic class Main {\n    public static void main(String[] args) {\n        // Should be in quotes\n        Json.readString(\"fozzie\");\n    }\n}\n```\n\n```java\nException in thread \"main\" dev.mccue.json.JsonReadException: JSON error (unexpected character): f\n\tat dev.mccue.json.JsonReadException.unexpectedCharacter(JsonReadException.java:33)\n\tat dev.mccue.json.internal.JsonReaderMethods.readStream(JsonReaderMethods.java:525)\n\tat dev.mccue.json.internal.JsonReaderMethods.read(JsonReaderMethods.java:533)\n\tat dev.mccue.json.internal.JsonReaderMethods.readFullyConsume(JsonReaderMethods.java:543)\n\tat dev.mccue.json.Json.readString(Json.java:369)\n\tat dev.mccue.json.Json.readString(Json.java:364)\n\tat dev.mccue.example.Main.main(Main.java:9)\n```\n\n### Decoding\n\nUp to this point, everything has been more or less the same as it is for other \"tree-based\"\nJSON libraries like [org.json](https://github.com/stleary/JSON-java) or [json-simple](https://github.com/fangyidong/json-simple).\n\nThis is where that will start to change.\n\nTo take some `Json` and turn it into a user defined class, a basic approach would be to use `instanceof` checks to see if\nthe `Json` is a particular subtype and navigate from there.\n\n```java\nimport dev.mccue.json.*;\n\nrecord Muppet(String name, boolean canSpeak) {\n    static Muppet fromJson(Json json) {\n        if (json instanceof JsonObject object \u0026\u0026\n            object.get(\"name\") instanceof JsonString name \u0026\u0026\n            object.get(\"canSpeak\") instanceof JsonBoolean canSpeak) {\n            return new Muppet(name.toString(), canSpeak.value());\n        }\n        else {\n            throw new RuntimeException(\"Invalid Muppet\");\n        }\n    }\n}\n\npublic class Main {\n    public static void main(String[] args) {\n        var json = Json.readString(\"\"\"\n                {\n                    \"name\": \"animal\",\n                    \"canSpeak\": false\n                }\n                \"\"\");\n\n        var animal = Muppet.fromJson(json);\n\n        System.out.println(animal);\n    }\n}\n```\n\nThis process is \"decoding.\" You \"read\" your data into JSON and then \"decode\"\nit to some type you define.\n\nThe problem with the `instanceof` approach is that you will end up with bad error messages on unexpected data.\nIn this case the error message would just be `\"Invalid Muppet\"`. The code to get better errors is tedious to write\nand I haven't seen many folks in the wild do it.\n\nTo get good errors, you should use the static methods defined in `JsonDecoder`.\n\n```java\npackage dev.mccue.example;\n\nimport dev.mccue.json.*;\n\nrecord Muppet(String name, boolean canSpeak) {\n    static Muppet fromJson(Json json) {\n        return new Muppet(\n                JsonDecoder.field(\n                        json,\n                        \"name\", \n                        JsonDecoder::string\n                ),\n                JsonDecoder.field(\n                        json, \n                        \"canSpeak\", \n                        JsonDecoder::boolean_\n                )\n        );\n    }\n}\n\npublic class Main {\n    public static void main(String[] args) {\n        var json = Json.readString(\"\"\"\n                {\n                    \"name\": \"animal\",\n                    \"canSpeak\": false\n                }\n                \"\"\");\n\n        var animal = Muppet.fromJson(json);\n\n        System.out.println(animal);\n    }\n}\n```\n\nThese handle the fiddly process of checking whether the JSON matches the structure you\nexpect and throwing an appropriate error.\n\nYou should read this declaration as \"at the field `name` I expect a string.\"\n\n```java\nJsonDecoder.field(json, \"name\", JsonDecoder::string)\n```\n\nIf the JSON is not an object, or doesn't have a value for `name`, or that value\nis not a string, you will get a `JsonDecodeException`.\n\n```java\npublic class Main {\n    public static void main(String[] args) {\n        var json = Json.readString(\"\"\"\n                {\n                    \"canSpeak\": false\n                }\n                \"\"\");\n\n        var animal = JsonDecoder.field(\n                json, \n                \"name\", \n                JsonDecoder::string\n        );\n\n        System.out.println(animal);\n    }\n}\n```\nWhich will have a message indicating exactly what went wrong and where.\n\n```java \nProblem with the value at json.name:\n\n    {\n        \"canSpeak\": false\n    }\n\nno value for field\n```\n\nThe last argument to `JsonDecoder.field` is the `JsonDecoder` you want to use to interpret the value at that field.\nIn this case a method reference to `JsonDecoder.string`, which is a method that asserts JSON is a string\nand throws if it isn't.\n\nFor the methods which take more than one argument, there are overloads\nwhich can be used to get an instance of `JsonDecoder`.\n\n```java\n// This will actually decode the json into a list of strings\nList\u003cString\u003e items = JsonDecoder.array(json, JsonDecoder::string);\n\n// This will just return a decoder\nDecoder\u003cList\u003cString\u003e\u003e decoder = \n        JsonDecoder.array(JsonDecoder::string);\n```\n\nThis, in conjunction with `JsonDecoder.field` is how you are intended to explore nested paths.\n\n```java\npublic class Main {\n    public static void main(String[] args) {\n        var json = Json.readString(\"\"\"\n                {\n                    \"villains\": [\"constantine\", \"doc hopper\"]\n                }\n                \"\"\");\n\n        List\u003cString\u003e villains = JsonDecoder.field(\n                json,\n                \"villains\",\n                JsonDecoder.array(JsonDecoder::string)\n        );\n\n        System.out.println(villains);\n    }\n}\n```\n\nTo decode JSON into your custom classes, you should add either a constructor or\na static factory method which takes in `Json` and use these decoders to make your objects.\n\n```java\nimport dev.mccue.json.*;\n\nimport java.util.List;\n\nrecord Actor(String name, String role, boolean muppet) {\n    static Actor fromJson(Json json) {\n        return new Actor(\n                JsonDecoder.field(json, \"name\", JsonDecoder::string),\n                JsonDecoder.field(json, \"role\", JsonDecoder::string),\n                JsonDecoder.optionalField(\n                        json, \n                        \"muppet\",\n                        JsonDecoder::boolean_,\n                        true\n                )\n        );\n    }\n}\n\n\nrecord Movie(String title, List\u003cActor\u003e cast) {\n    static Movie fromJson(Json json) {\n        return new Movie(\n                JsonDecoder.field(json, \"title\", JsonDecoder::string),\n                JsonDecoder.field(\n                        json, \n                        \"cast\", \n                        JsonDecoder.array(Actor::fromJson)\n                )\n        );\n    }\n}\n\npublic class Main {\n    public static void main(String[] args) {\n        var json = Json.readString(\"\"\"\n                 {\n                     \"title\": \"Treasure Island\",\n                     \"cast\": [\n                         {\n                             \"name\": \"Kermit\",\n                             \"role\": \"The Captain\"\n                         },\n                         {\n                             \"name\": \"Tim Curry\",\n                             \"role\": \"Long John Silver\",\n                             \"muppet\": false\n                         }\n                     ]\n                 }\n                 \"\"\");\n\n        var movie = Movie.fromJson(json);\n\n        System.out.println(movie);\n    }\n}\n```\n\n### Full Round-Trip\n\nWith all of that out of the way, here is how you might define a model,\nwrite it to json, and read it back in.\n\n```java\nimport dev.mccue.json.*;\n\nimport java.util.List;\n\nrecord Actor(String name, String role, boolean muppet)\n    implements JsonEncodable {\n    static Actor fromJson(Json json) {\n        return new Actor(\n                JsonDecoder.field(json, \"name\", JsonDecoder::string),\n                JsonDecoder.field(json, \"role\", JsonDecoder::string),\n                JsonDecoder.optionalField(\n                        json,\n                        \"muppet\",\n                        JsonDecoder::boolean_,\n                        true)\n        );\n    }\n\n    @Override\n    public Json toJson() {\n        return Json.objectBuilder()\n                .put(\"name\", name)\n                .put(\"role\", role)\n                .put(\"muppet\", muppet)\n                .build();\n    }\n}\n\n\nrecord Movie(String title, List\u003cActor\u003e cast)\n    implements JsonEncodable {\n    static Movie fromJson(Json json) {\n        return new Movie(\n                JsonDecoder.field(json, \"title\", JsonDecoder::string),\n                JsonDecoder.field(\n                        json, \n                        \"cast\", \n                        JsonDecoder.array(Actor::fromJson)\n                )\n        );\n    }\n\n    @Override\n    public Json toJson() {\n        return Json.objectBuilder()\n                .put(\"title\", title)\n                .put(\"cast\", cast)\n                .build();\n    }\n}\n\npublic class Main {\n    public static void main(String[] args) {\n        var json = Json.readString(\"\"\"\n                 {\n                     \"title\": \"Treasure Island\",\n                     \"cast\": [\n                         {\n                             \"name\": \"Kermit\",\n                             \"role\": \"The Captain\",\n                             \"muppet\": true\n                         },\n                         {\n                             \"name\": \"Tim Curry\",\n                             \"role\": \"Long John Silver\",\n                             \"muppet\": false\n                         }\n                     ]\n                 }\n                 \"\"\");\n\n        var movie = Movie.fromJson(json);\n\n        var roundTrippedJson = Json.readString(\n                Json.writeString(movie.toJson())\n        );\n        var roundTrippedMovie = Movie.fromJson(roundTrippedJson);\n\n        System.out.println(\n                json.equals(roundTrippedJson)\n        );\n\n        System.out.println(\n                movie.equals(roundTrippedMovie)\n        );\n    }\n}\n```\n\u003c/details\u003e\n\n\n## Examples\n\n### Create Json from a String\n\n\u003cdetails\u003e\n    \u003csummary\u003eShow\u003c/summary\u003e\n\n```java\nimport dev.mccue.json.Json;\nimport dev.mccue.json.JsonObject;\n\npublic class Main {\n   public static void main(String[] args) {\n      Json line = Json.of(\"rainbow connection\");\n\n      System.out.println(line);\n   }\n}\n```\n\u003c/details\u003e\n\n### Create Json from Numbers\n\n\u003cdetails\u003e\n    \u003csummary\u003eShow\u003c/summary\u003e\n\n```java\nimport dev.mccue.json.Json;\nimport dev.mccue.json.JsonArray;\n\nimport java.math.BigDecimal;\nimport java.math.BigInteger;\nimport java.util.List;\n\npublic class Main {\n   public static void main(String[] args) {\n      JsonArray numbers = JsonArray.of(\n              Json.of(1),\n              Json.of(2L),\n              Json.of(3.5),\n              Json.of(new BigInteger(\"4\")),\n              Json.of(new BigDecimal(\"5.5\"))\n      );\n\n\n      System.out.println(numbers);\n   }\n}\n```\n\u003c/details\u003e\n\n### Create a JsonObject\n\n\u003cdetails\u003e\n    \u003csummary\u003eShow\u003c/summary\u003e\n\n```java\nimport dev.mccue.json.Json;\nimport dev.mccue.json.JsonObject;\n\npublic class Main {\n   public static void main(String[] args) {\n      JsonObject swedishChef = Json.objectBuilder()\n              .put(\"name\", \"chef\")\n              .put(\"nationality\", \"swedish\")\n              .put(\"lines\", 1)\n              .build();\n\n      System.out.println(swedishChef);\n   }\n}\n```\n\u003c/details\u003e\n\n### Create a JsonArray\n\n\u003cdetails\u003e\n    \u003csummary\u003eShow\u003c/summary\u003e\n\n```java\nimport dev.mccue.json.Json;\nimport dev.mccue.json.JsonArray;\n\npublic class Main {\n   public static void main(String[] args) {\n      JsonArray lonelyNumbers = Json.arrayBuilder()\n              .add(1)\n              .add(2)\n              .build();\n\n      System.out.println(lonelyNumbers);\n   }\n}\n```\n\u003c/details\u003e\n\n### Create a nested structure\n\n\u003cdetails\u003e\n    \u003csummary\u003eShow\u003c/summary\u003e\n\n```java\nimport dev.mccue.json.Json;\nimport dev.mccue.json.JsonObject;\n\npublic class Main {\n   public static void main(String[] args) {\n      JsonObject kermit = Json.objectBuilder()\n              .put(\"name\", \"kermit\")\n              .put(\"wife\", Json.objectBuilder()\n                      .put(\"name\", \"ms piggy\"))\n              .put(\"children\", Json.arrayBuilder()\n                      .add(Json.objectBuilder()\n                              .put(\"species\", \"frog\")\n                              .put(\"gender\", \"male\"))\n                      .add(Json.objectBuilder()\n                              .put(\"species\", \"pig\")\n                              .put(\"gender\", \"female\")))\n              .put(\"commitmentIssues\", true)\n              .build();\n\n      System.out.println(kermit);\n   }\n}\n```\n\u003c/details\u003e\n\n### Read from a String\n\n\u003cdetails\u003e\n    \u003csummary\u003eShow\u003c/summary\u003e\n\n\n\n```java\nimport dev.mccue.json.Json;\n\npublic class Main {\n    public static void main(String[] args) {\n        Json parsed = Json.readString(\"\"\"\n                {\n                    \"name\": \"Tiny Tim\",\n                    \"cute\": true\n                }\n                \"\"\");\n\n        System.out.println(parsed);\n    }\n}\n```\n\u003c/details\u003e\n\n### Read from a file\n\n\u003cdetails\u003e\n    \u003csummary\u003eShow\u003c/summary\u003e\n\n```java\nimport dev.mccue.json.Json;\n\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\n\npublic class Main {\n    public static void main(String[] args) throws IOException {\n        Json parsed;\n        try (var reader = Files.newBufferedReader(Path.of(\"in.json\"))) {\n            parsed = Json.read(reader);\n        }\n\n        System.out.println(parsed);\n    }\n}\n```\n\n\u003c/details\u003e\n\n### Read multiple top level forms\n\n\u003cdetails\u003e\n    \u003csummary\u003eShow\u003c/summary\u003e\n\n```java\nimport dev.mccue.json.Json;\n\nimport java.io.StringReader;\n\npublic class Main {\n    public static void main(String[] args) {\n        String source = \"\"\"\n                { \"name\": \"gonzo\" }\n                { \"name\": \"kermit\" }\n                { \"name\": \"ms. piggy\" }\n                \"\"\";\n\n        var reader = Json.reader(new StringReader(source));\n\n        for (var muppet : reader) {\n            System.out.println(muppet);\n        }\n    }\n}\n```\n\n\u003c/details\u003e\n\n### Write to a String\n\n\u003cdetails\u003e\n    \u003csummary\u003eShow\u003c/summary\u003e\n\n```java\nimport dev.mccue.json.Json;\n\npublic class Main {\n   public static void main(String[] args) {\n      Json beaker = Json.objectBuilder()\n              .put(\"name\", \"Beaker\")\n              .put(\"milliliters\", 5)\n              .put(\"scientist\", true)\n              .build();\n\n      String written = Json.writeString(beaker);\n\n      System.out.println(written);\n   }\n}\n```\n\n```\n{\"name\":\"Beaker\",\"milliliters\":5,\"scientist\":true}\n```\n\n\u003c/details\u003e\n\n### Write to a String with indentation\n\n\u003cdetails\u003e\n    \u003csummary\u003eShow\u003c/summary\u003e\n\n```java\nimport dev.mccue.json.Json;\nimport dev.mccue.json.JsonWriteOptions;\n\npublic class Main {\n    public static void main(String[] args) {\n        Json beaker = Json.objectBuilder()\n                .put(\"name\", \"Beaker\")\n                .put(\"milliliters\", 5)\n                .put(\"scientist\", true)\n                .build();\n        \n        String written = Json.writeString(\n                beaker,\n                new JsonWriteOptions()\n                        .withIndentation(4)\n        );\n\n        System.out.println(written);\n    }\n}\n```\n\n```\n{\n    \"name\": \"Beaker\",\n    \"milliliters\": 5,\n    \"scientist\": true\n}\n```\n\n\u003c/details\u003e\n\n### Write to a file\n\n\u003cdetails\u003e\n    \u003csummary\u003eShow\u003c/summary\u003e\n\n```java \nimport dev.mccue.json.Json;\n\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\n\npublic class Main {\n    public static void main(String[] args) throws IOException {\n        Json bunsen = Json.objectBuilder()\n                .put(\"name\", \"bunsen\")\n                .put(\"scientist\", true)\n                .build();\n\n        try (var writer = Files.newBufferedWriter(\n                Path.of(\"out.json\")\n        )) {\n            Json.write(bunsen, writer);\n        }\n    }\n}\n```\n\n\u003c/details\u003e\n\n### Encode a basic object\n\n\u003cdetails\u003e\n    \u003csummary\u003eShow\u003c/summary\u003e\n\n```java\nimport dev.mccue.json.Json;\nimport dev.mccue.json.JsonEncodable;\nimport dev.mccue.json.JsonWriteOptions;\n\nrecord Muppet(String name, boolean canSing)\n        implements JsonEncodable {\n\n    @Override\n    public Json toJson() {\n        return Json.objectBuilder()\n                .put(\"name\", this.name)\n                .put(\"canSing\", this.canSing)\n                .build();\n    }\n}\n\npublic class Main {\n    public static void main(String[] args) {\n        var animal = new Muppet(\"animal\", false);\n        System.out.println(Json.writeString(\n                animal,\n                new JsonWriteOptions()\n                        .withIndentation(4)\n        ));\n    }\n}\n```\n\n```\n{\n    \"name\": \"animal\",\n    \"canSing\": false\n}\n```\n\n\u003c/details\u003e\n\n### Encode nested objects\n\n\u003cdetails\u003e\n    \u003csummary\u003eShow\u003c/summary\u003e\n\n```java\nimport dev.mccue.json.Json;\nimport dev.mccue.json.JsonEncodable;\nimport dev.mccue.json.JsonWriteOptions;\n\nimport java.util.List;\n\nrecord Muppet(String name)\n        implements JsonEncodable {\n\n    @Override\n    public Json toJson() {\n        return Json.objectBuilder()\n                .put(\"name\", this.name)\n                .build();\n    }\n}\n\nrecord Movie(String title, List\u003cMuppet\u003e cast)\n        implements JsonEncodable {\n\n    @Override\n    public Json toJson() {\n        return Json.objectBuilder()\n                .put(\"title\", this.title)\n                .put(\"cast\", this.cast)\n                .build();\n    }\n}\n\npublic class Main {\n    public static void main(String[] args) {\n        var kermit = new Muppet(\"kermit\");\n        var gonzo = new Muppet(\"gonzo\");\n        var rizzo = new Muppet(\"rizzo\");\n\n        var treasureIsland = new Movie(\n                \"Treasure Island\",\n                List.of(kermit, gonzo, rizzo)\n        );\n\n        System.out.println(Json.writeString(\n                treasureIsland,\n                new JsonWriteOptions()\n                        .withIndentation(4)\n        ));\n    }\n}\n```\n\n```\n{\n    \"title\": \"Treasure Island\",\n    \"cast\": [\n        {\n            \"name\": \"kermit\"\n        },\n        {\n            \"name\": \"gonzo\"\n        },\n        {\n            \"name\": \"rizzo\"\n        }\n    ]\n}\n```\n\n\u003c/details\u003e\n\n### Encode objects of classes that you didn't make\n\n\u003cdetails\u003e\n    \u003csummary\u003eShow\u003c/summary\u003e\n\n```java\npublic class Main {\n    static Json encodeInstant(Instant instant) {\n        return Json.of(DateTimeFormatter.ISO_INSTANT.format(instant));\n    }\n\n    public static void main(String[] args) {\n        Json instant = encodeInstant(Instant.now());\n        System.out.println(Json.writeString(instant));\n    }\n}\n```\n\n\u003c/details\u003e\n\n### Decode a basic object\n\n\u003cdetails\u003e\n    \u003csummary\u003eShow\u003c/summary\u003e\n\n```java\nimport dev.mccue.json.Json;\nimport dev.mccue.json.JsonDecoder;\n\nrecord Muppet(String name) {\n    static Muppet fromJson(Json json) {\n        var name = JsonDecoder.field(json, \"name\", JsonDecoder::string);\n        return new Muppet(name);\n    }\n}\n\npublic class Main {\n    public static void main(String[] args) {\n        var jsonString = \"\"\"\n                [\n                    {\n                        \"name\": \"kermit\"\n                    },\n                    {\n                        \"name\": \"gonzo\"\n                    },\n                    {\n                        \"name\": \"rizzo\"\n                    }\n                ]\n                \"\"\";\n        var json = Json.readString(jsonString);\n\n        var muppets = JsonDecoder.array(json, Muppet::fromJson);\n\n        System.out.println(muppets);\n    }\n}\n```\n\n\u003c/details\u003e\n\n### Decode an object with optional fields.\n\n\u003cdetails\u003e\n    \u003csummary\u003eShow\u003c/summary\u003e\n\n```java\nimport dev.mccue.json.Json;\nimport dev.mccue.json.JsonDecoder;\n\nrecord Muppet(String name, String role) {\n    static Muppet fromJson(Json json) {\n        var name = JsonDecoder.field(json, \"name\", JsonDecoder::string);\n        var role = JsonDecoder.optionalField(json, \"role\", JsonDecoder::string, \"sidekick\");\n        return new Muppet(name, role);\n    }\n}\n\n\npublic class Main {\n    public static void main(String[] args) {\n        var jsonString = \"\"\"\n                [\n                    {\n                        \"name\": \"kermit\",\n                        \"role\": \"captain\"\n                    },\n                    {\n                        \"name\": \"gonzo\"\n                    },\n                    {\n                        \"name\": \"rizzo\"\n                    }\n                ]\n                \"\"\";\n        var json = Json.readString(jsonString);\n        var muppets = JsonDecoder.array(json, Muppet::fromJson);\n\n        System.out.println(muppets);\n    }\n}\n```\n\n\u003c/details\u003e\n\n### Decode nested objects\n\n\u003cdetails\u003e\n    \u003csummary\u003eShow\u003c/summary\u003e\n\n```java\nimport dev.mccue.json.Json;\nimport dev.mccue.json.JsonDecoder;\n\nimport java.util.List;\n\nrecord Muppet(String name) {\n    static Muppet fromJson(Json json) {\n        return new Muppet(JsonDecoder.field(json, \"name\", JsonDecoder::string));\n    }\n\n}\n\nrecord Movie(String title, List\u003cMuppet\u003e cast) {\n    static Movie fromJson(Json json) {\n        return new Movie(\n                JsonDecoder.field(json, \"title\", JsonDecoder::string),\n                JsonDecoder.field(json, \"cast\", JsonDecoder.array(Muppet::fromJson))\n        );\n    }\n}\n\npublic class Main {\n    public static void main(String[] args) {\n        var jsonString = \"\"\"\n                {\n                    \"title\": \"Treasure Island\",\n                    \"cast\": [\n                        {\n                            \"name\": \"kermit\"\n                        },\n                        {\n                            \"name\": \"gonzo\"\n                        },\n                        {\n                            \"name\": \"rizzo\"\n                        }\n                    ]\n                }\n                \"\"\";\n        var json = Json.readString(jsonString);\n        var movie = Movie.fromJson(json);\n\n        System.out.println(movie);\n    }\n}\n```\n\n\u003c/details\u003e\n\n### Decode an enum\n\n\u003cdetails\u003e\n    \u003csummary\u003eShow\u003c/summary\u003e\n\n```java\nimport dev.mccue.json.Json;\nimport dev.mccue.json.JsonDecoder;\n\nimport java.util.List;\n\nenum Location {\n   CALIFORNIA,\n   RHODE_ISLAND,\n   SASKATCHEWAN,\n   NEW_YORK;\n\n   static Location fromJson(Json json) {\n      return Location.valueOf(JsonDecoder.string(json));\n   }\n}\n\npublic class Main {\n   public static void main(String[] args) {\n      Json locationsJson = Json.readString(\"\"\"\n              [\n                  \"CALIFORNIA\",\n                  \"SASKATCHEWAN\"\n              ]\n              \"\"\");\n\n      List\u003cLocation\u003e locations = JsonDecoder.array(\n              locationsJson,\n              Location::fromJson\n      );\n\n      System.out.println(locations);\n   }\n}\n```\n\n\u003c/details\u003e\n\n### Decode json with a fixed set of keys\n\n\u003cdetails\u003e\n    \u003csummary\u003eShow\u003c/summary\u003e\n\n```java\nimport dev.mccue.json.Json;\nimport dev.mccue.json.JsonDecodeException;\nimport dev.mccue.json.JsonDecoder;\n\nimport java.util.HashSet;\nimport java.util.Set;\n\nrecord Prison(String location) {\n    public static Prison fromJson(Json json) {\n        var object = JsonDecoder.object(json);\n        var expected = Set.of(\"location\");\n        if (!expected.equals(object.keySet())) {\n            var extra = new HashSet\u003c\u003e(object.keySet());\n            extra.removeAll(expected);\n            throw JsonDecodeException.of(\"Extra Keys: \" + extra, json);\n        }\n\n        return new Prison(\n                JsonDecoder.field(json, \"location\", JsonDecoder::string)\n        );\n    }\n}\npublic class Main {\n    public static void main(String[] args) {\n        Json withExtraKeys = Json.readString(\n                \"\"\"\n                        {\n                            \"location\": \"Siberia\",\n                            \"escapeMethod\": \"tunnelling\"\n                        }\n                        \"\"\"\n        );\n\n        var prison = Prison.fromJson(withExtraKeys);\n    }\n}\n```\n\n\u003c/details\u003e\n\n\n### Decode json into bean\n\n\u003cdetails\u003e\n    \u003csummary\u003eShow\u003c/summary\u003e\n\n⚠️ This example is just intended to show how you can use decoders to make objects\nthat have different construction methods. Don't mindlessly add getters\nand setters to your classes!\n```java\nimport dev.mccue.json.Json;\nimport dev.mccue.json.JsonDecoder;\n\nimport java.util.List;\n\nclass Fozzie {\n    private String joke;\n    private String punchline;\n    private List\u003cString\u003e hecklers;\n\n    public Fozzie() {}\n\n    public String getJoke() {\n        return joke;\n    }\n\n    public void setJoke(String joke) {\n        this.joke = joke;\n    }\n\n    public String getPunchline() {\n        return punchline;\n    }\n\n    public void setPunchline(String punchline) {\n        this.punchline = punchline;\n    }\n\n    public List\u003cString\u003e getHecklers() {\n        return hecklers;\n    }\n\n    public void setHecklers(List\u003cString\u003e hecklers) {\n        this.hecklers = hecklers;\n    }\n\n    @Override\n    public String toString() {\n        return \"Fozzie[\" +\n                \"joke=\" + joke +\n                \", punchline=\" + punchline  +\n                \", hecklers=\" + hecklers +\n                ']';\n    }\n}\n\npublic class Main {\n    static Fozzie fozzieFromJson(Json json) {\n        var fozzie = new Fozzie();\n        fozzie.setJoke(JsonDecoder.field(json, \"joke\", JsonDecoder::string));\n        fozzie.setPunchline(JsonDecoder.field(json, \"punchline\", JsonDecoder::string));\n        fozzie.setHecklers(JsonDecoder.field(\n                json, \n                \"hecklers\", \n                JsonDecoder.array(JsonDecoder::string)\n        ));\n        return fozzie;\n    }\n\n    public static void main(String[] args) {\n        Json fozzieJson = Json.readString(\"\"\"\n                {\n                    \"joke\": \"What do you get when you cross the Atlantic with the titanic?\",\n                    \"punchline\": \"Halfway! Wacka Wacka!\",\n                    \"hecklers\": [\"Statler\", \"Waldorf\"]\n                }\n                \"\"\");\n\n        Fozzie fozzie = fozzieFromJson(fozzieJson);\n\n        System.out.println(fozzie);\n    }\n}\n```\n\n\u003c/details\u003e\n\n### Decode from multiple representations\n\n\u003cdetails\u003e\n    \u003csummary\u003eShow\u003c/summary\u003e\n\n```java\nimport dev.mccue.json.Json;\nimport dev.mccue.json.JsonDecoder;\n\nimport java.util.List;\n\nrecord Person(String firstName, String lastName) {\n    static Person fromJsonV1(Json json) {\n        var fullName = JsonDecoder.field(json, \"name\", JsonDecoder::string);\n        var split = fullName.split(\" \", 2);\n        return new Person(split[0], split[1]);\n    }\n\n    static Person fromJsonV2(Json json) {\n        return new Person(\n                JsonDecoder.field(json, \"first_name\", JsonDecoder::string),\n                JsonDecoder.field(json, \"last_name\", JsonDecoder::string)\n        );\n    }\n\n    static Person fromJson(Json json) {\n        return JsonDecoder.oneOf(\n                json,\n                Person::fromJsonV2,\n                Person::fromJsonV1\n        );\n    }\n}\n\npublic class Main {\n    public static void main(String[] args) {\n        Json peopleJson = Json.readString(\"\"\"\n                [\n                    {\n                        \"name\": \"Great Gonzo\"\n                    },\n                    {\n                        \"first_name\": \"Jim\",\n                        \"last_name\": \"Henson\"\n                    }\n                ]\n                \"\"\");\n\n        List\u003cPerson\u003e people = JsonDecoder.array(peopleJson, Person::fromJson);\n\n        System.out.println(people);\n    }\n}\n```\n\n```\n[Person[firstName=Great, lastName=Gonzo], Person[firstName=Jim, lastName=Henson]]\n```\n\n\u003c/details\u003e\n\n### Usage from Spring\n\n\u003cdetails\u003e\n    \u003csummary\u003eShow\u003c/summary\u003e\n\n#### Step 1. Add a new jackson module as a bean\n\n```java\nimport com.fasterxml.jackson.core.JsonGenerator;\nimport com.fasterxml.jackson.core.JsonParser;\nimport com.fasterxml.jackson.databind.DeserializationContext;\nimport com.fasterxml.jackson.databind.JsonNode;\nimport com.fasterxml.jackson.databind.Module;\nimport com.fasterxml.jackson.databind.SerializerProvider;\nimport com.fasterxml.jackson.databind.deser.std.StdDeserializer;\nimport com.fasterxml.jackson.databind.module.SimpleModule;\nimport com.fasterxml.jackson.databind.ser.std.StdSerializer;\nimport dev.mccue.json.*;\nimport dev.mccue.json.stream.JsonWriteable;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\n\nimport java.io.IOException;\nimport java.io.UncheckedIOException;\n\n@Configuration\npublic class McCueJsonModule {\n    @Bean\n    public Module jsonSerializer() {\n        var module = new SimpleModule();\n        module.addSerializer(new StdSerializer\u003c\u003e(JsonWriteable.class) {\n            @Override\n            public void serialize(\n                    JsonWriteable writeable,\n                    JsonGenerator jsonGenerator,\n                    SerializerProvider serializerProvider\n            ) throws IOException {\n                try {\n                    writeable.write(new ProxyWriter(jsonGenerator));\n                } catch (UncheckedIOException e) {\n                    throw e.getCause();\n                }\n            }\n        });\n        module.addDeserializer(Json.class, new StdDeserializer\u003c\u003e(Json.class) {\n            private Json deserializeTree(JsonNode tree) {\n                if (tree.isTextual()) {\n                    return JsonString.of(tree.textValue());\n                }\n                else if (tree.isNull()) {\n                    return JsonNull.instance();\n                }\n                else if (tree.isBoolean()) {\n                    return JsonBoolean.of(tree.booleanValue());\n                }\n                else if (tree.isLong()) {\n                    return JsonNumber.of(tree.longValue());\n                }\n                else if (tree.isDouble()) {\n                    return JsonNumber.of(tree.doubleValue());\n                }\n                else if (tree.isBigDecimal()) {\n                    return JsonNumber.of(tree.decimalValue());\n                }\n                else if (tree.isBigInteger()) {\n                    return JsonNumber.of(tree.bigIntegerValue());\n                }\n                else if (tree.isArray()) {\n                    var arrayBuilder = JsonArray.builder();\n                    for (var value : tree) {\n                        arrayBuilder.add(deserializeTree(value));\n                    }\n                    return arrayBuilder.build();\n                }\n                else if (tree.isObject()) {\n                    var objectBuilder = JsonObject.builder();\n                    var fieldNamesIter = tree.fieldNames();\n                    while (fieldNamesIter.hasNext()) {\n                        var fieldName = fieldNamesIter.next();\n                        objectBuilder.put(fieldName, deserializeTree(tree.get(fieldName)));\n                    }\n                    return objectBuilder.build();\n                }\n                else {\n                    throw new IllegalStateException(\"Should have handled all JsonNode types?\");\n                }\n            }\n\n            @Override\n            public Json deserialize(JsonParser jsonParser, DeserializationContext deserializationContext)\n                    throws IOException {\n                return deserializeTree(jsonParser.readValueAsTree());\n            }\n        });\n        return module;\n    }\n\n    private record ProxyWriter(JsonGenerator jsonGenerator)\n            implements dev.mccue.json.stream.JsonGenerator {\n        @Override\n        public void writeObjectStart() {\n            try {\n                jsonGenerator.writeStartObject();\n            } catch (IOException e) {\n                throw new UncheckedIOException(e);\n            }\n        }\n\n        @Override\n        public void writeObjectEnd() {\n            try {\n                jsonGenerator.writeEndObject();\n            } catch (IOException e) {\n                throw new UncheckedIOException(e);\n            }\n        }\n\n        @Override\n        public void writeArrayStart() {\n            try {\n                jsonGenerator.writeStartArray();\n            } catch (IOException e) {\n                throw new UncheckedIOException(e);\n            }\n        }\n\n        @Override\n        public void writeArrayEnd() {\n            try {\n                jsonGenerator.writeEndArray();\n            } catch (IOException e) {\n                throw new UncheckedIOException(e);\n            }\n        }\n\n        @Override\n        public void writeFieldName(String s) {\n            try {\n                jsonGenerator.writeFieldName(s);\n            } catch (IOException e) {\n                throw new UncheckedIOException(e);\n            }\n        }\n\n        @Override\n        public void writeString(String s) {\n            try {\n                jsonGenerator.writeString(s);\n            } catch (IOException e) {\n                throw new UncheckedIOException(e);\n            }\n        }\n\n        @Override\n        public void writeNumber(JsonNumber jsonNumber) {\n            try {\n                jsonGenerator.writeNumber(jsonNumber.toString());\n            } catch (IOException e) {\n                throw new UncheckedIOException(e);\n            }\n        }\n\n        @Override\n        public void writeTrue() {\n            try {\n                jsonGenerator.writeBoolean(true);\n            } catch (IOException e) {\n                throw new UncheckedIOException(e);\n            }\n        }\n\n        @Override\n        public void writeFalse() {\n            try {\n                jsonGenerator.writeBoolean(false);\n            } catch (IOException e) {\n                throw new UncheckedIOException(e);\n            }\n        }\n\n        @Override\n        public void writeNull() {\n            try {\n                jsonGenerator.writeNull();\n            } catch (IOException e) {\n                throw new UncheckedIOException(e);\n            }\n        }\n    }\n}\n```\n\n#### Step 2. Annotate your creation methods with @JsonCreator\n\n```java\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport dev.mccue.json.Json;\nimport dev.mccue.json.JsonDecoder;\nimport dev.mccue.json.JsonEncodable;\n\npublic record Muppet(String name) implements JsonEncodable {\n\n    @Override\n    public Json toJson() {\n        return Json.objectBuilder()\n                .put(\"name\", name)\n                .build();\n    }\n\n    @JsonCreator\n    public static Muppet fromJson(Json json) {\n        return new Muppet(JsonDecoder.field(json, \"name\", JsonDecoder::string));\n    }\n}\n```\n\n\n\u003c/details\u003e\n\n### Encode and Decode with Kotlin \n\n\u003cdetails\u003e\n    \u003csummary\u003eShow\u003c/summary\u003e\n\n```kotlin\nimport dev.mccue.json.Json\nimport dev.mccue.json.JsonDecoder\nimport dev.mccue.json.JsonEncodable\nimport dev.mccue.json.JsonWriteOptions\n\ndata class Muppet(\n   val name: String,\n   val scientist: Boolean,\n   val lines: String?\n) : JsonEncodable {\n   override fun toJson(): Json =\n      Json.objectBuilder()\n         .put(\"name\", name)\n         .put(\"scientist\", scientist)\n         .put(\"lines\", lines)\n         .build()\n\n   companion object {\n      fun fromJson(json: Json): Muppet =\n         Muppet(\n            JsonDecoder.field(json, \"name\") { JsonDecoder.string(it) },\n            JsonDecoder.field(json, \"scientist\") { JsonDecoder.boolean_(it) },\n            JsonDecoder.nullableField(json,\n               \"lines\",\n               { JsonDecoder.string(it) },\n               null\n            )\n         )\n   }\n}\n\ndata class Movie(\n   val title: String,\n   val muppets: List\u003cMuppet\u003e\n) : JsonEncodable {\n   override fun toJson(): Json {\n      return Json.objectBuilder()\n         .put(\"title\", title)\n         .put(\"muppets\", muppets)\n         .build()\n   }\n\n   companion object {\n      fun fromJson(json: Json): Movie =\n         Movie(\n            JsonDecoder.field(json, \"title\") { JsonDecoder.string(it) },\n            JsonDecoder.field(json, \"muppets\", JsonDecoder.array { Muppet.fromJson(it) })\n         )\n   }\n}\n\n\nfun main(args: Array\u003cString\u003e) {\n   val movie = Movie(\n      \"Most wanted\",\n      listOf(\n         Muppet(\n            \"kermit\",\n            false,\n            \"I'm not Constantine!\"\n         ),\n         Muppet(\n            \"beaker\",\n            true,\n            null\n         ),\n         Muppet(\n            \"bunsen\",\n            true,\n            \"I don't mean to be a stickler\"\n         )\n      )\n   )\n\n   println(Json.writeString(movie, JsonWriteOptions().withIndentation(4)))\n\n   val movieRoundTripped = Movie.fromJson(Json.readString(Json.writeString(movie)))\n\n   println(movieRoundTripped)\n   println(movie)\n   println(movie == movieRoundTripped)\n}\n```\n\n```\n{\n    \"title\": \"Most wanted\",\n    \"muppets\": [\n        {\n            \"name\": \"kermit\",\n            \"scientist\": false,\n            \"lines\": \"I'm not Constantine!\"\n        },\n        {\n            \"name\": \"beaker\",\n            \"scientist\": true,\n            \"lines\": null\n        },\n        {\n            \"name\": \"bunsen\",\n            \"scientist\": true,\n            \"lines\": \"I don't mean to be a stickler\"\n        }\n    ]\n}\nMovie(title=Most wanted, muppets=[Muppet(name=kermit, scientist=false, lines=I'm not Constantine!), Muppet(name=beaker, scientist=true, lines=null), Muppet(name=bunsen, scientist=true, lines=I don't mean to be a stickler)])\nMovie(title=Most wanted, muppets=[Muppet(name=kermit, scientist=false, lines=I'm not Constantine!), Muppet(name=beaker, scientist=true, lines=null), Muppet(name=bunsen, scientist=true, lines=I don't mean to be a stickler)])\ntrue\n```\n\n\u003c/details\u003e\n\n### Encode and Decode with Scala 3\n\n\u003cdetails\u003e\n    \u003csummary\u003eShow\u003c/summary\u003e\n\n```scala\nimport dev.mccue.json.{Json, JsonDecoder, JsonEncodable, JsonWriteOptions}\n\nimport scala.jdk.CollectionConverters._\n\ncase class Muppet(name: String, scientist: Boolean, lines: Option[String]) extends JsonEncodable {\n  override def toJson: Json =\n    Json.objectBuilder()\n      .put(\"name\", name)\n      .put(\"scientist\", scientist)\n      .put(\"lines\", lines.orNull)\n      .build\n}\n\nobject Muppet {\n  def fromJson(json: Json): Muppet =\n    Muppet(\n      JsonDecoder.field(json, \"name\", JsonDecoder.string _),\n      JsonDecoder.field(json, \"scientist\", JsonDecoder.boolean_ _),\n      JsonDecoder.nullableField(json, \"name\", JsonDecoder.string _)\n        .map(Option(_))\n        .orElse(None)\n    )\n}\n\ncase class Movie(title: String, muppets: Seq[Muppet]) extends JsonEncodable {\n  override def toJson: Json =\n    Json.objectBuilder()\n      .put(\"title\", title)\n      .put(\"muppets\", muppets.asJava)\n      .build()\n}\n\nobject Movie {\n  def fromJson(json: Json): Movie =\n    Movie(\n      JsonDecoder.field(json, \"title\", JsonDecoder.string _),\n      JsonDecoder.field(json, \"muppets\", JsonDecoder.array(Muppet.fromJson _))\n        .asScala\n        .toSeq\n    )\n}\n\n\n@main\ndef main(): Unit = {\n  val movie = Movie(\n    \"Most wanted\",\n    Seq(\n      Muppet(\n        \"kermit\",\n        false,\n        Some(\"I'm not Constantine!\")\n      ),\n      Muppet(\n        \"beaker\",\n        true,\n        None\n      ),\n      Muppet(\n        \"bunsen\",\n        true,\n        Some(\"I don't mean to be a stickler\")\n      )\n    )\n  )\n\n  println(Json.writeString(movie, JsonWriteOptions().withIndentation(4)))\n\n  val movieRoundTripped = Movie.fromJson(Json.readString(Json.writeString(movie)))\n\n  println(movieRoundTripped)\n  println(movie)\n  println(movie == movieRoundTripped)\n}\n```\n\n``` \n{\n    \"title\": \"Most wanted\",\n    \"muppets\": [\n        {\n            \"name\": \"kermit\",\n            \"scientist\": false,\n            \"lines\": \"I'm not Constantine!\"\n        },\n        {\n            \"name\": \"beaker\",\n            \"scientist\": true,\n            \"lines\": null\n        },\n        {\n            \"name\": \"bunsen\",\n            \"scientist\": true,\n            \"lines\": \"I don't mean to be a stickler\"\n        }\n    ]\n}\nMovie(Most wanted,List(Muppet(kermit,false,Some(kermit)), Muppet(beaker,true,Some(beaker)), Muppet(bunsen,true,Some(bunsen))))\nMovie(Most wanted,List(Muppet(kermit,false,Some(I'm not Constantine!)), Muppet(beaker,true,None), Muppet(bunsen,true,Some(I don't mean to be a stickler))))\nfalse\n```\n\u003c/details\u003e\n\n### Decode into sealed trait with Scala 3\n\n\u003cdetails\u003e\n    \u003csummary\u003eShow\u003c/summary\u003e\n\n```scala\nimport dev.mccue.json.{Json, JsonDecodeException, JsonDecoder, JsonEncodable, JsonWriteOptions}\n\nimport scala.jdk.CollectionConverters.*\n\nsealed trait MessageBody {\n  def messageId: Int\n}\n\nobject MessageBody {\n  def fromJson(json: Json): MessageBody = {\n    val id = JsonDecoder.field(json, \"type\", JsonDecoder.string _)\n    id match\n      case \"init\" =\u003e\n        Init(\n          JsonDecoder.field(json, \"msg_id\", JsonDecoder.int_ _),\n          JsonDecoder.field(json, \"node_id\", JsonDecoder.string _),\n          JsonDecoder.field(json, \"node_ids\", JsonDecoder.array(JsonDecoder.string _))\n            .asScala\n            .toSeq\n        )\n      case \"init_ok\" =\u003e\n        InitOk(\n          JsonDecoder.field(json, \"msg_id\", JsonDecoder.int_ _),\n          JsonDecoder.field(json, \"in_reply_to\", JsonDecoder.int_ _)\n        )\n      case _ =\u003e\n        throw JsonDecodeException.atField(\"type\", JsonDecodeException.of(\n          \"expected one of \\\"init\\\", \\\"init_ok\\\"\",\n          json\n        ))\n  }\n}\n\ncase class Init(messageId: Int, nodeId: String, nodeIds: Seq[String]) extends MessageBody {}\ncase class InitOk(messageId: Int, inReplyTo: Int) extends MessageBody {}\n\ncase class Envelope(src: Option[String], dest: Option[String], body: MessageBody) {}\n\nobject Envelope {\n  def fromJson(json: Json): Envelope = {\n    Envelope(\n      JsonDecoder.optionalField(json, \"src\", JsonDecoder.string _)\n        .map(Option(_))\n        .orElse(None),\n\n      JsonDecoder.optionalField(json, \"dest\", JsonDecoder.string _)\n        .map(Option(_))\n        .orElse(None),\n\n      JsonDecoder.field(json, \"body\", MessageBody.fromJson _)\n    )\n  }\n}\n```\n\n\u003c/details\u003e\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbowbahdoe%2Fjson","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbowbahdoe%2Fjson","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbowbahdoe%2Fjson/lists"}