From e3903ecda50217685c1291e465d178b5a5ed76ff Mon Sep 17 00:00:00 2001
From: Ethan McCue
Date: Wed, 1 Mar 2023 02:35:34 -0500
Subject: [PATCH 1/3] Fix nullable decoder and add more tests
---
src/main/java/dev/mccue/json/JsonArray.java | 3 -
src/main/java/dev/mccue/json/JsonDecoder.java | 13 +-
.../serialization/JsonSerializationProxy.java | 2 +-
.../java/dev/mccue/json/JsonDecoderTest.java | 128 +++++++++++++++++-
src/test/java/dev/mccue/json/JsonTest.java | 15 ++
.../dev/mccue/json/SerializationTest.java | 115 +++++++++++++++-
6 files changed, 268 insertions(+), 8 deletions(-)
diff --git a/src/main/java/dev/mccue/json/JsonArray.java b/src/main/java/dev/mccue/json/JsonArray.java
index 759f2eb..d4671e9 100644
--- a/src/main/java/dev/mccue/json/JsonArray.java
+++ b/src/main/java/dev/mccue/json/JsonArray.java
@@ -8,7 +8,6 @@
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
-import java.util.Objects;
/**
* Represents an array in the json data model.
@@ -19,8 +18,6 @@ static JsonArray of(Json... values) {
}
static JsonArray of(List value) {
- Objects.requireNonNull(value, "Json.Array value must be nonnull");
- value.forEach(json -> Objects.requireNonNull(json, "Each value in a Json.Array must be nonnull"));
return new ArrayImpl(List.copyOf(value));
}
diff --git a/src/main/java/dev/mccue/json/JsonDecoder.java b/src/main/java/dev/mccue/json/JsonDecoder.java
index 4b1d712..9dc5324 100644
--- a/src/main/java/dev/mccue/json/JsonDecoder.java
+++ b/src/main/java/dev/mccue/json/JsonDecoder.java
@@ -172,6 +172,17 @@ static T null_(Json json) throws JsonDecodeException {
}
}
+ static T null_(Json json, T value) throws JsonDecodeException {
+ if (!(json instanceof JsonNull)) {
+ throw JsonDecodeException.of(
+ "expected null",
+ json
+ );
+ } else {
+ return value;
+ }
+ }
+
static JsonDecoder> array(JsonDecoder extends T> itemDecoder) throws JsonDecodeException {
return json -> array(json, itemDecoder);
}
@@ -420,7 +431,7 @@ static JsonDecoder nullable(JsonDecoder extends T> decoder, T defaultVa
return json -> JsonDecoder.oneOf(
json,
decoder,
- __ -> defaultValue
+ JsonDecoder.of(JsonDecoder::null_).map(__ -> defaultValue)
);
}
diff --git a/src/main/java/dev/mccue/json/serialization/JsonSerializationProxy.java b/src/main/java/dev/mccue/json/serialization/JsonSerializationProxy.java
index fdc4955..6cc3c98 100644
--- a/src/main/java/dev/mccue/json/serialization/JsonSerializationProxy.java
+++ b/src/main/java/dev/mccue/json/serialization/JsonSerializationProxy.java
@@ -20,7 +20,7 @@
* freely.
*
*
- * @param json
+ * @param json A string representation of the JSON being serialized.
*/
public record JsonSerializationProxy(String json) implements Serializable {
@Serial
diff --git a/src/test/java/dev/mccue/json/JsonDecoderTest.java b/src/test/java/dev/mccue/json/JsonDecoderTest.java
index 678cec8..cdec6e3 100644
--- a/src/test/java/dev/mccue/json/JsonDecoderTest.java
+++ b/src/test/java/dev/mccue/json/JsonDecoderTest.java
@@ -2,12 +2,14 @@
import org.junit.jupiter.api.Test;
+import java.math.BigDecimal;
+import java.math.BigInteger;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
+import java.util.Optional;
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.*;
public class JsonDecoderTest {
@Test
@@ -84,4 +86,126 @@ static Person fromJson(Json json) {
)
);
}
+
+ @Test
+ public void nullableDecoderTest() {
+ assertEquals(
+ Optional.of("abc"),
+ JsonDecoder.nullable(JsonDecoder::string)
+ .decode(JsonString.of("abc"))
+ );
+ assertEquals(
+ Optional.empty(),
+ JsonDecoder.nullable(JsonDecoder::string)
+ .decode(JsonNull.instance())
+ );
+ assertThrows(
+ JsonDecodeException.class,
+ () -> JsonDecoder.nullable(JsonDecoder::string)
+ .decode(JsonArray.of())
+ );
+
+ assertEquals(
+ "abc",
+ JsonDecoder.nullable(JsonDecoder::string, null)
+ .decode(JsonString.of("abc"))
+ );
+ assertNull(JsonDecoder.nullable(JsonDecoder::string, null)
+ .decode(JsonNull.instance()));
+ assertEquals(
+ "def",
+ JsonDecoder.nullable(JsonDecoder::string, "def")
+ .decode(JsonNull.instance())
+ );
+ assertThrows(
+ JsonDecodeException.class,
+ () -> JsonDecoder.nullable(JsonDecoder::string, null)
+ .decode(JsonArray.of())
+ );
+ }
+
+ @Test
+ public void booleanDecoderTest() {
+ assertTrue(JsonDecoder.boolean_(JsonTrue.instance()));
+ assertFalse(JsonDecoder.boolean_(JsonFalse.instance()));
+ assertThrows(
+ JsonDecodeException.class,
+ () -> JsonDecoder.boolean_(JsonString.of("abc"))
+ );
+ }
+
+ @Test
+ public void stringDecoderTest() {
+ assertEquals("abc", JsonDecoder.string(JsonString.of("abc")));
+ assertThrows(
+ JsonDecodeException.class,
+ () -> JsonDecoder.string(JsonNull.instance())
+ );
+ }
+
+ @Test
+ public void intDecoderTest() {
+ assertEquals(1, JsonDecoder.int_(Json.of(1)));
+ assertEquals(2, JsonDecoder.int_(Json.of(2L)));
+ assertEquals(3, JsonDecoder.int_(Json.of(new BigInteger("3"))));
+ assertEquals(4, JsonDecoder.int_(Json.of(new BigDecimal("4"))));
+ assertThrows(
+ JsonDecodeException.class,
+ () -> JsonDecoder.int_(Json.of(Long.MAX_VALUE))
+ );
+ assertThrows(
+ JsonDecodeException.class,
+ () -> JsonDecoder.int_(Json.of("abc"))
+ );
+ }
+
+ @Test
+ public void longDecoderTest() {
+ assertEquals(1L, JsonDecoder.long_(Json.of(1)));
+ assertEquals(2L, JsonDecoder.long_(Json.of(2L)));
+ assertEquals(3L, JsonDecoder.long_(Json.of(new BigInteger("3"))));
+ assertEquals(4L, JsonDecoder.long_(Json.of(new BigDecimal("4"))));
+ assertThrows(
+ JsonDecodeException.class,
+ () -> JsonDecoder.long_(Json.of(new BigDecimal("43242523525235235255")))
+ );
+ assertThrows(
+ JsonDecodeException.class,
+ () -> JsonDecoder.long_(Json.of("abc"))
+ );
+ }
+
+ @Test
+ public void floatDecoderTest() {
+ assertEquals(1f, JsonDecoder.float_(Json.of(1)));
+ assertEquals(2f, JsonDecoder.float_(Json.of(2L)));
+ assertEquals(3f, JsonDecoder.float_(Json.of(new BigInteger("3"))));
+ assertEquals(4f, JsonDecoder.float_(Json.of(new BigDecimal("4"))));
+ assertThrows(
+ JsonDecodeException.class,
+ () -> JsonDecoder.long_(Json.of("abc"))
+ );
+ }
+
+ @Test
+ public void doubleDecoderTest() {
+ assertEquals(1L, JsonDecoder.double_(Json.of(1)));
+ assertEquals(2L, JsonDecoder.double_(Json.of(2L)));
+ assertEquals(3L, JsonDecoder.double_(Json.of(new BigInteger("3"))));
+ assertEquals(4L, JsonDecoder.double_(Json.of(new BigDecimal("4"))));
+ assertThrows(
+ JsonDecodeException.class,
+ () -> JsonDecoder.double_(Json.of("abc"))
+ );
+ }
+
+ @Test
+ public void nullDecoderTest() {
+ assertNull(JsonDecoder.null_(JsonNull.instance()));
+ assertEquals(5, JsonDecoder.null_(JsonNull.instance(), 5));
+ assertThrows(
+ JsonDecodeException.class,
+ () -> JsonDecoder.null_(Json.of("abc"))
+ );
+ }
}
diff --git a/src/test/java/dev/mccue/json/JsonTest.java b/src/test/java/dev/mccue/json/JsonTest.java
index 5afd753..7165e16 100644
--- a/src/test/java/dev/mccue/json/JsonTest.java
+++ b/src/test/java/dev/mccue/json/JsonTest.java
@@ -78,4 +78,19 @@ public void testOfNumbers() {
public void testOfString() {
assertEquals(Json.of("abc"), JsonString.of("abc"));
}
+
+ @Test
+ public void testTrueRepr() {
+ assertEquals("true", JsonTrue.instance().toString());
+ }
+
+ @Test
+ public void testFalseRepr() {
+ assertEquals("false", JsonFalse.instance().toString());
+ }
+
+ @Test
+ public void testNullRepr() {
+ assertEquals("null", JsonNull.instance().toString());
+ }
}
diff --git a/src/test/java/dev/mccue/json/SerializationTest.java b/src/test/java/dev/mccue/json/SerializationTest.java
index c0fcb16..4c8ab9e 100644
--- a/src/test/java/dev/mccue/json/SerializationTest.java
+++ b/src/test/java/dev/mccue/json/SerializationTest.java
@@ -1,11 +1,14 @@
package dev.mccue.json;
+import dev.mccue.json.serialization.JsonSerializationProxy;
import org.junit.jupiter.api.Test;
import java.io.*;
+import java.util.Arrays;
+import java.util.Map;
import java.util.Objects;
-import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.*;
public class SerializationTest {
@Test
@@ -32,4 +35,114 @@ public void testSerialization() throws IOException, ClassNotFoundException {
assertEquals(json, ois.readObject());
}
}
+
+ @Test
+ public void testNonsenseForm() throws IOException, ClassNotFoundException {
+ byte[] data;
+ try (var baos = new ByteArrayOutputStream();
+ var oos = new ObjectOutputStream(baos)) {
+ oos.writeObject(new JsonSerializationProxy(""));
+ oos.writeObject(new JsonSerializationProxy("{\"a\":"));
+ data = baos.toByteArray();
+ }
+
+ try (var bais = new ByteArrayInputStream(data);
+ var ois = new ObjectInputStream(bais)) {
+ assertThrows(JsonReadException.class, ois::readObject);
+ assertThrows(JsonReadException.class, ois::readObject);
+ }
+ }
+
+ @Test
+ public void testMalicousStream() throws IOException, ClassNotFoundException {
+ byte[] jsonNull = {
+ -84, -19, 0, 5, 115, 114, 0, 23, 100, 101, 118, 46, 109, 99, 99, 117, 101, 46, 106, 115, 111, 110, 46, 74, 115, 111, 110, 78, 117, 108, 108, 0, 0, 0, 0, 0, 0, 0, 1, 2, 0, 0, 120, 112
+ };
+
+ byte[] jsonTrue = {
+ -84, -19, 0, 5, 115, 114, 0, 23, 100, 101, 118, 46, 109, 99, 99, 117, 101, 46, 106, 115, 111, 110, 46, 74, 115, 111, 110, 84, 114, 117, 101, 0, 0, 0, 0, 0, 0, 0, 1, 2, 0, 0, 120, 112
+ };
+
+ byte[] jsonFalse = {
+ -84, -19, 0, 5, 115, 114, 0, 24, 100, 101, 118, 46, 109, 99, 99, 117, 101, 46, 106, 115, 111, 110, 46, 74, 115, 111, 110, 70, 97, 108, 115, 101, 0, 0, 0, 0, 0, 0, 0, 1, 2, 0, 0, 120, 112
+ };
+
+ /*byte[] data;
+ try (var baos = new ByteArrayOutputStream();
+ var oos = new ObjectOutputStream(baos)) {
+ oos.writeObject(JsonFalse.instance());
+ data = baos.toByteArray();
+ }
+
+ System.out.println(Arrays.toString(data));*/
+
+
+ try (var ois = new ObjectInputStream(new ByteArrayInputStream(jsonNull))) {
+ assertThrows(IllegalStateException.class, ois::readObject);
+ }
+
+ try (var ois = new ObjectInputStream(new ByteArrayInputStream(jsonTrue))) {
+ assertThrows(IllegalStateException.class, ois::readObject);
+ }
+
+ try (var ois = new ObjectInputStream(new ByteArrayInputStream(jsonFalse))) {
+ assertThrows(IllegalStateException.class, ois::readObject);
+ }
+ }
+
+ private Object roundTrip(Object o) throws IOException, ClassNotFoundException {
+ byte[] data;
+ try (var baos = new ByteArrayOutputStream();
+ var oos = new ObjectOutputStream(baos)) {
+ oos.writeObject(o);
+ data = baos.toByteArray();
+ }
+
+ try (var bais = new ByteArrayInputStream(data);
+ var ois = new ObjectInputStream(bais)) {
+ return ois.readObject();
+ }
+ }
+
+ @Test
+ public void roundTripTrue() throws IOException, ClassNotFoundException {
+ assertEquals(JsonTrue.instance(), roundTrip(JsonTrue.instance()));
+ assertSame(JsonTrue.instance(), roundTrip(JsonTrue.instance()));
+ }
+
+ @Test
+ public void roundTripFalse() throws IOException, ClassNotFoundException {
+ assertEquals(JsonFalse.instance(), roundTrip(JsonFalse.instance()));
+ assertSame(JsonFalse.instance(), roundTrip(JsonFalse.instance()));
+ }
+
+ @Test
+ public void roundTripNull() throws IOException, ClassNotFoundException {
+ assertEquals(JsonNull.instance(), roundTrip(JsonNull.instance()));
+ assertSame(JsonNull.instance(), roundTrip(JsonNull.instance()));
+ }
+
+ @Test
+ public void roundTripString() throws IOException, ClassNotFoundException {
+ assertEquals(JsonString.of("abc"), roundTrip(JsonString.of("abc")));
+ }
+
+ @Test
+ public void roundTripArray() throws IOException, ClassNotFoundException {
+ var o = JsonArray.of(
+ JsonString.of("abc"),
+ JsonNull.instance()
+ );
+ assertEquals(o, roundTrip(o));
+ }
+
+ @Test
+ public void roundTripObject() throws IOException, ClassNotFoundException {
+ var o = JsonObject.of(Map.of(
+ "abc", JsonString.of("def"),
+ "ghi", JsonNull.instance(),
+ "kjl", JsonArray.of()
+ ));
+ assertEquals(o, roundTrip(o));
+ }
}
From 63a7a8e04f2416556622a5fb6c315df44180458d Mon Sep 17 00:00:00 2001
From: Ethan McCue
Date: Wed, 1 Mar 2023 02:37:45 -0500
Subject: [PATCH 2/3] Add tutorial and bump version
---
README.md | 777 +++++++++++++++++++++++++++++++++++++++++++++++++++++-
pom.xml | 2 +-
2 files changed, 776 insertions(+), 3 deletions(-)
diff --git a/README.md b/README.md
index 6dabaf8..46b0db3 100644
--- a/README.md
+++ b/README.md
@@ -16,7 +16,7 @@ Requires Java 17+.
dev.mccue
json
- 0.2.1
+ 0.2.2
```
@@ -24,7 +24,7 @@ Requires Java 17+.
```
dependencies {
- implementation("dev.mccue:json:0.2.1")
+ implementation("dev.mccue:json:0.2.2")
}
```
@@ -42,6 +42,779 @@ The non-goals of this library are
2. Support every extension to the JSON spec.
3. Handle documents which cannot fit into memory.
+## Tutorial
+
+
+ Show
+
+### The Data Model
+
+JSON is a data format. It looks like the following sample.
+
+```json
+{
+ "name": "kermit",
+ "wife": null,
+ "girlfriend": "Ms. Piggy",
+ "age": 22,
+ "children": [
+ {
+ "species": "frog",
+ "gender": "male"
+ },
+ {
+ "species": "pig",
+ "gender": "female"
+ }
+ ],
+ "commitmentIssues": true
+}
+```
+
+In JSON you represent data using a combination of objects (maps from strings to JSON),
+arrays (ordered sequences of JSON), strings, numbers, true, false, and null.
+
+Therefore, one "natural" way to think about the data stored in a JSON document
+is as the union of those possibilities.
+
+```
+JSON is one of
+- a map of string to JSON
+- a list of JSON
+- a string
+- a number
+- true
+- false
+- null
+```
+
+The way to represent this in Java is using a sealed interface, which
+provides an explicit list of types which are allowed to implement it.
+
+```java
+public sealed interface Json
+ permits
+ JsonObject,
+ JsonArray,
+ JsonString,
+ JsonNumber,
+ JsonBoolean,
+ JsonNull {
+}
+```
+
+This means that if you have a field or variable which has the type `Json`, you know
+that it is either a `JsonObject`, `JsonArray`, `JsonString`, `JsonNumber`, `JsonBoolean`,
+or `JsonNull`.
+
+That is the first thing provided by my library. There is a `Json` type
+and subtypes representing those different cases.
+
+```java
+import dev.mccue.json.*;
+
+public class Main {
+ static Json greeting() {
+ return JsonString.of("hello");
+ }
+
+ public static void main(String[] args) {
+ Json json = greeting();
+ switch (json) {
+ case JsonObject object ->
+ System.out.println("An object");
+ case JsonArray array ->
+ System.out.println("An array");
+ case JsonString str ->
+ System.out.println("A string");
+ case JsonNumber number ->
+ System.out.println("A number");
+ case JsonBoolean bool ->
+ System.out.println("A boolean");
+ case JsonNull __ ->
+ System.out.println("A json null");
+ }
+ }
+}
+```
+
+You can create instances
+of these subtypes using factory methods on the types themselves.
+
+```java
+import dev.mccue.json.*;
+
+import java.util.List;
+import java.util.Map;
+
+public class Main {
+ public static void main(String[] args) {
+ JsonObject kermit = JsonObject.of(Map.of(
+ "name", JsonString.of("kermit"),
+ "age", JsonNumber.of(22),
+ "commitmentIssues", JsonBoolean.of(true),
+ "wife", JsonNull.instance(),
+ "children", JsonArray.of(List.of(
+ JsonString.of("Tiny Tim")
+ ))
+ ));
+
+ System.out.println(kermit);
+ }
+}
+```
+
+Or by using factory methods on `Json`, which aren't guaranteed to give you
+any specific subtype but in exchange will handle converting any stray `null`s to `JsonNull`.
+
+```java
+import dev.mccue.json.*;
+
+import java.util.List;
+import java.util.Map;
+
+public class Main {
+ public static void main(String[] args) {
+ Json kermit = Json.of(Map.of(
+ "name", Json.of("kermit"),
+ "age", Json.of(22),
+ "commitmentIssues", Json.of(true),
+ "wife", Json.ofNull(),
+ "children", Json.of(List.of(
+ JsonString.of("Tiny Tim")
+ ))
+ ));
+
+ System.out.println(kermit);
+ }
+}
+```
+
+For `JsonObject` and `JsonArray`, there also use builders available which
+can make it so that you don't need to write `Json.of` on every value.
+
+```java
+import dev.mccue.json.Json;
+
+public class Main {
+ public static void main(String[] args) {
+ Json kermit = Json.objectBuilder()
+ .put("name", "kermit")
+ .put("age", 22)
+ .putTrue("commitmentIssues")
+ .putNull("wife")
+ .put("children", Json.arrayBuilder()
+ .add("Tiny Tim"))
+ .build();
+
+ System.out.println(kermit);
+ }
+}
+```
+
+### Writing
+
+Once you have some `Json` you can write it out to a `String` using `Json.writeString`
+
+```java
+import dev.mccue.json.Json;
+
+public class Main {
+ public static void main(String[] args) {
+ Json songJson = Json.objectBuilder()
+ .put("title", "Rainbow Connection")
+ .put("year", 1979)
+ .build();
+
+ String song = Json.writeString(songJson);
+ System.out.println(song);
+ }
+}
+```
+
+```json
+{"title":"Rainbow Connection","year":1979}
+```
+
+If output is meant to be consumed by humans then whitespace can be added
+using a customized instance of `JsonWriteOptions`.
+
+```java
+import dev.mccue.json.Json;
+import dev.mccue.json.JsonWriteOptions;
+
+public class Main {
+ public static void main(String[] args) {
+ Json songJson = Json.objectBuilder()
+ .put("title", "Rainbow Connection")
+ .put("year", 1979)
+ .build();
+
+ String song = Json.writeString(
+ songJson,
+ new JsonWriteOptions()
+ .withIndentation(4)
+ );
+
+ System.out.println(song);
+ }
+}
+```
+
+```json
+{
+ "title": "Rainbow Connection",
+ "year": 1979
+}
+```
+
+If you want to write JSON to something other than a `String`, you need to
+obtain a `Writer` and use `Json.write`.
+
+```java
+import dev.mccue.json.Json;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+public class Main {
+ public static void main(String[] args) throws IOException {
+ Json songJson = Json.objectBuilder()
+ .put("title", "Rainbow Connection")
+ .put("year", 1979)
+ .build();
+
+
+ try (var fileWriter = Files.newBufferedWriter(
+ Path.of("song.json"))
+ ) {
+ Json.write(songJson, fileWriter);
+ }
+ }
+}
+```
+
+### Encoding
+
+To turn a class you have defined into JSON, you just need to make a method
+which creates an instance of `Json` from the data stored in your class.
+
+```java
+import dev.mccue.json.Json;
+
+record Muppet(String name) {
+ Json toJson() {
+ return Json.objectBuilder()
+ .put("name", name)
+ .build();
+ }
+}
+
+public class Main {
+ public static void main(String[] args) {
+ var beaker = new Muppet("beaker");
+ Json beakerJson = beaker.toJson();
+
+ System.out.println(Json.writeString(beakerJson));
+ }
+}
+```
+
+This process is "encoding." You "encode" your data into JSON and then "write"
+that JSON to some output.
+
+For classes that you did not define, the logic for the conversion just needs to live somewhere.
+Dealer's choice where, but static methods are generally a good call.
+
+```java
+import dev.mccue.json.Json;
+
+import java.time.Month;
+import java.time.MonthDay;
+import java.time.format.DateTimeFormatter;
+
+final class TimeEncoders {
+ private TimeEncoders() {}
+
+ static Json monthDayToJson(MonthDay monthDay) {
+ return Json.of(
+ DateTimeFormatter.ofPattern("MM-dd")
+ .format(monthDay)
+ );
+ }
+}
+
+record Muppet(String name, MonthDay birthday) {
+ Json toJson() {
+ return Json.objectBuilder()
+ .put("name", name)
+ .put(
+ "birthday",
+ TimeEncoders.monthDayToJson(birthday)
+ )
+ .build();
+ }
+}
+
+public class Main {
+ public static void main(String[] args) {
+ var elmo = new Muppet(
+ "Elmo",
+ MonthDay.of(Month.FEBRUARY, 3)
+ );
+ Json elmoJson = elmo.toJson();
+
+ System.out.println(Json.writeString(elmoJson));
+ }
+}
+```
+
+```json
+{"name":"Elmo","birthday":"02-03"}
+```
+
+If a class you define has a JSON representation that could be considered "canonical", the interface `JsonEncodable`
+can be implemented. This will let you pass an instance of the class directly to `Json.writeString` or `Json.write`.
+
+```java
+import dev.mccue.json.Json;
+import dev.mccue.json.JsonEncodable;
+
+record Muppet(String name, boolean great)
+ implements JsonEncodable {
+ @Override
+ public Json toJson() {
+ return Json.objectBuilder()
+ .put("name", name)
+ .put("great", great)
+ .build();
+ }
+}
+
+public class Main {
+ public static void main(String[] args) {
+ var gonzo = new Muppet("Gonzo", true);
+ System.out.println(Json.writeString(gonzo));
+ }
+}
+```
+
+### Reading
+
+The inverse of writing JSON is reading it.
+
+If you have some JSON stored in a `String` you can
+read it into `Json` using `Json.readString`.
+
+```java
+import dev.mccue.json.Json;
+
+public class Main {
+ public static void main(String[] args) {
+ Json movie = Json.readString("""
+ {
+ "title": "Treasure Island",
+ "cast": [
+ {
+ "name": "Kermit",
+ "role": "The Captain",
+ "muppet": true
+ },
+ {
+ "name": "Tim Curry",
+ "role": "Long John Silver",
+ "muppet": false
+ }
+ ]
+
+ }
+ """);
+
+ System.out.println(movie);
+ }
+}
+```
+
+If that JSON is coming from another source, you need to obtain a `Reader` and use `Json.read`.
+
+```java
+import dev.mccue.json.Json;
+
+import java.io.IOException;
+import java.io.Reader;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+public class Main {
+ public static void main(String[] args) throws IOException {
+ // If you were following along, we created this earlier!
+ Json song;
+ try (Reader fileReader = Files.newBufferedReader(
+ Path.of("song.json"))
+ ) {
+ song = Json.read(fileReader);
+ }
+
+ System.out.println(song);
+ }
+}
+```
+
+If the JSON you provide is malformed in some way, a `JsonReadException` will be thrown.
+
+```java
+import dev.mccue.json.Json;
+
+public class Main {
+ public static void main(String[] args) {
+ // Should be in quotes
+ Json.readString("fozzie");
+ }
+}
+```
+
+```java
+Exception in thread "main" dev.mccue.json.JsonReadException: JSON error (unexpected character): f
+ at dev.mccue.json.JsonReadException.unexpectedCharacter(JsonReadException.java:33)
+ at dev.mccue.json.internal.JsonReaderMethods.readStream(JsonReaderMethods.java:525)
+ at dev.mccue.json.internal.JsonReaderMethods.read(JsonReaderMethods.java:533)
+ at dev.mccue.json.internal.JsonReaderMethods.readFullyConsume(JsonReaderMethods.java:543)
+ at dev.mccue.json.Json.readString(Json.java:369)
+ at dev.mccue.json.Json.readString(Json.java:364)
+ at dev.mccue.example.Main.main(Main.java:9)
+```
+
+### Decoding
+
+Up to this point, everything has been more or less the same as it is for other "tree-based"
+JSON libraries like [org.json](https://github.com/stleary/JSON-java) or [json-simple](https://github.com/fangyidong/json-simple).
+
+This is where that will start to change.
+
+To take some `Json` and turn it into a user defined class, a basic approach would be to use `instanceof` checks to see if
+the `Json` is a particular subtype and navigate from there.
+
+```java
+import dev.mccue.json.*;
+
+record Muppet(String name, boolean canSpeak) {
+ static Muppet fromJson(Json json) {
+ if (json instanceof JsonObject object &&
+ object.get("name") instanceof JsonString name &&
+ object.get("canSpeak") instanceof JsonBoolean canSpeak) {
+ return new Muppet(name.toString(), canSpeak.value());
+ }
+ else {
+ throw new RuntimeException("Invalid Muppet");
+ }
+ }
+}
+
+public class Main {
+ public static void main(String[] args) {
+ var json = Json.readString("""
+ {
+ "name": "animal",
+ "canSpeak": false
+ }
+ """);
+
+ var animal = Muppet.fromJson(json);
+
+ System.out.println(animal);
+ }
+}
+```
+
+This process is "decoding." You "read" your data into JSON and then "decode"
+it to some type you define.
+
+The problem with the `instanceof` approach is that you will end up with bad error messages on unexpected data.
+In this case the error message would just be `"Invalid Muppet"`. The code to get better errors is tedious to write
+and I haven't seen many folks in the wild do it.
+
+To get good errors, you should use the static methods defined in `JsonDecoder`.
+
+```java
+package dev.mccue.example;
+
+import dev.mccue.json.*;
+
+record Muppet(String name, boolean canSpeak) {
+ static Muppet fromJson(Json json) {
+ return new Muppet(
+ JsonDecoder.field(
+ json,
+ "name",
+ JsonDecoder::string
+ ),
+ JsonDecoder.field(
+ json,
+ "canSpeak",
+ JsonDecoder::boolean_
+ )
+ );
+ }
+}
+
+public class Main {
+ public static void main(String[] args) {
+ var json = Json.readString("""
+ {
+ "name": "animal",
+ "canSpeak": false
+ }
+ """);
+
+ var animal = Muppet.fromJson(json);
+
+ System.out.println(animal);
+ }
+}
+```
+
+These handle the fiddly process of checking whether the JSON matches the structure you
+expect and throwing an appropriate error.
+
+You should read this declaration as "at the field `name` I expect a string."
+
+```java
+JsonDecoder.field(json, "name", JsonDecoder::string)
+```
+
+If the JSON is not an object, or doesn't have a value for `name`, or that value
+is not a string, you will get a `JsonDecodeException`.
+
+```java
+public class Main {
+ public static void main(String[] args) {
+ var json = Json.readString("""
+ {
+ "canSpeak": false
+ }
+ """);
+
+ var animal = JsonDecoder.field(
+ json,
+ "name",
+ JsonDecoder::string
+ );
+
+ System.out.println(animal);
+ }
+}
+```
+Which will have a message indicating exactly what went wrong and where.
+
+```java
+Problem with the value at json.name:
+
+ {
+ "canSpeak": false
+ }
+
+no value for field
+```
+
+The last argument to `JsonDecoder.field` is the `JsonDecoder` you want to use to interpret the value at that field.
+In this case a method reference to `JsonDecoder.string`, which is a method that asserts JSON is a string
+and throws if it isn't.
+
+For the methods which take more than one argument, there are overloads
+which can be used to get an instance of `JsonDecoder`.
+
+```java
+// This will actually decode the json into a list of strings
+List items = JsonDecoder.array(json, JsonDecoder::string);
+
+// This will just return a decoder
+Decoder> decoder =
+ JsonDecoder.array(JsonDecoder::string);
+```
+
+This, in conjunction with `JsonDecoder.field` is how you are intended to explore nested paths.
+
+```java
+public class Main {
+ public static void main(String[] args) {
+ var json = Json.readString("""
+ {
+ "villains": ["constantine", "doc hopper"]
+ }
+ """);
+
+ List villains = JsonDecoder.field(
+ json,
+ "villains",
+ JsonDecoder.array(JsonDecoder::string)
+ );
+
+ System.out.println(villains);
+ }
+}
+```
+
+To decode JSON into your custom classes, you should add either a constructor or
+a static factory method which takes in `Json` and use these decoders to make your objects.
+
+```java
+import dev.mccue.json.*;
+
+import java.util.List;
+
+record Actor(String name, String role, boolean muppet) {
+ static Actor fromJson(Json json) {
+ return new Actor(
+ JsonDecoder.field(json, "name", JsonDecoder::string),
+ JsonDecoder.field(json, "role", JsonDecoder::string),
+ JsonDecoder.optionalField(
+ json,
+ "muppet",
+ JsonDecoder::boolean_,
+ true
+ )
+ );
+ }
+}
+
+
+record Movie(String title, List cast) {
+ static Movie fromJson(Json json) {
+ return new Movie(
+ JsonDecoder.field(json, "title", JsonDecoder::string),
+ JsonDecoder.field(
+ json,
+ "cast",
+ JsonDecoder.array(Actor::fromJson)
+ )
+ );
+ }
+}
+
+public class Main {
+ public static void main(String[] args) {
+ var json = Json.readString("""
+ {
+ "title": "Treasure Island",
+ "cast": [
+ {
+ "name": "Kermit",
+ "role": "The Captain"
+ },
+ {
+ "name": "Tim Curry",
+ "role": "Long John Silver",
+ "muppet": false
+ }
+ ]
+ }
+ """);
+
+ var movie = Movie.fromJson(json);
+
+ System.out.println(movie);
+ }
+}
+```
+
+### Full Round-Trip
+
+With all of that out of the way, here is how you might define a model,
+write it to json, and read it back in.
+
+```java
+import dev.mccue.json.*;
+
+import java.util.List;
+
+record Actor(String name, String role, boolean muppet)
+ implements JsonEncodable {
+ static Actor fromJson(Json json) {
+ return new Actor(
+ JsonDecoder.field(json, "name", JsonDecoder::string),
+ JsonDecoder.field(json, "role", JsonDecoder::string),
+ JsonDecoder.optionalField(
+ json,
+ "muppet",
+ JsonDecoder::boolean_,
+ true)
+ );
+ }
+
+ @Override
+ public Json toJson() {
+ return Json.objectBuilder()
+ .put("name", name)
+ .put("role", role)
+ .put("muppet", muppet)
+ .build();
+ }
+}
+
+
+record Movie(String title, List cast)
+ implements JsonEncodable {
+ static Movie fromJson(Json json) {
+ return new Movie(
+ JsonDecoder.field(json, "title", JsonDecoder::string),
+ JsonDecoder.field(
+ json,
+ "cast",
+ JsonDecoder.array(Actor::fromJson)
+ )
+ );
+ }
+
+ @Override
+ public Json toJson() {
+ return Json.objectBuilder()
+ .put("title", title)
+ .put("cast", cast)
+ .build();
+ }
+}
+
+public class Main {
+ public static void main(String[] args) {
+ var json = Json.readString("""
+ {
+ "title": "Treasure Island",
+ "cast": [
+ {
+ "name": "Kermit",
+ "role": "The Captain",
+ "muppet": true
+ },
+ {
+ "name": "Tim Curry",
+ "role": "Long John Silver",
+ "muppet": false
+ }
+ ]
+ }
+ """);
+
+ var movie = Movie.fromJson(json);
+
+ var roundTrippedJson = Json.readString(
+ Json.writeString(movie.toJson())
+ );
+ var roundTrippedMovie = Movie.fromJson(roundTrippedJson);
+
+ System.out.println(
+ json.equals(roundTrippedJson)
+ );
+
+ System.out.println(
+ movie.equals(roundTrippedMovie)
+ );
+ }
+}
+```
+
+
+
## Examples
### Create Json from a String
diff --git a/pom.xml b/pom.xml
index 78c3d69..a7c530c 100644
--- a/pom.xml
+++ b/pom.xml
@@ -6,7 +6,7 @@
dev.mccue
json
- 0.2.1
+ 0.2.2
jar
From b408517d0f2ad78b06e5137d89506ff1946aeaf9 Mon Sep 17 00:00:00 2001
From: Ethan McCue
Date: Wed, 1 Mar 2023 02:38:53 -0500
Subject: [PATCH 3/3] Make numbers example use JsonArray
---
README.md | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 46b0db3..e32be2c 100644
--- a/README.md
+++ b/README.md
@@ -843,6 +843,7 @@ public class Main {
```java
import dev.mccue.json.Json;
+import dev.mccue.json.JsonArray;
import java.math.BigDecimal;
import java.math.BigInteger;
@@ -850,7 +851,7 @@ import java.util.List;
public class Main {
public static void main(String[] args) {
- List numbers = List.of(
+ JsonArray numbers = JsonArray.of(
Json.of(1),
Json.of(2L),
Json.of(3.5),