Json
A Java JSON Library intended to be easy to learn and simple to teach
Install / Use
/learn @bowbahdoe/JsonREADME
json
<img src="./bopbop.png"></img>
A Java JSON Library intended to be easy to learn and simple to teach.
Requires Java 21+.
Dependency Information
Maven
<dependency>
<groupId>dev.mccue</groupId>
<artifactId>json</artifactId>
<version>2024.11.20</version>
</dependency>
Gradle
dependencies {
implementation("dev.mccue:json:2024.11.20")
}
What this does
The primary goals of this library are
- Be easy to learn and simple to teach.
- Have an API for decoding that is reasonably declarative and gives good feedback on unexpected input.
- Make use of modern Java features.
The non-goals of this library are
- Provide an API for data-binding.
- Support every extension to the JSON spec.
- Handle documents which cannot fit into memory.
Tutorial
<details> <summary>Show</summary>The Data Model
JSON is a data format. It looks like the following sample.
{
"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.
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.
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.
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 nulls to JsonNull.
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.
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
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);
}
}
{"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.
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);
}
}
{
"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.
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.
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.
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));
}
}
{"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.
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.
import dev.mccue.js
