Java developers have often envied JavaScript for its ease of parsing JSON. Although Java offers more robustness, it tends to involve more work and boilerplate code. Thanks to the Manifold project, Java now has the potential to outshine JavaScript in parsing and processing JSON files. Manifold is a revolutionary set of language extensions for Java that completely changes the way we handle JSON (and much more…).
Getting Started with Manifold
The code for this tutorial can be found on my GitHub page. Manifold is relatively young but already vast in its capabilities. You can learn more about the project on their website and Slack channel. To begin, you'll need to install the Manifold plugin, which is currently only available for JetBrains IDEs. The project supports LTS releases of Java, including the latest JDK 19.
We can install the plugin from IntelliJ/IDEAs settings UI by navigating to the marketplace and searching for Manifold. The plugin makes sure the IDE doesn’t collide with the work done by the Maven/Gradle plugin.
Manifold consists of multiple smaller projects, each offering a custom language extension. Today, we'll discuss one such extension, but there's much more to explore.
Setting Up a Maven Project
To demonstrate Manifold, we'll use a simple Maven project (it also works with Gradle). We first need to paste the current Manifold version from their website and add the necessary dependencies. The main dependency for JSON is the manifold-json-rt
dependency. Other dependencies can be added for YAML, XML, and CSV support. We need to add this to the pom.xml
file in the project.
I'm aware of the irony where the boilerplate reduction for JSON starts with a great deal of configuration in the Maven build script. But this is configuration, not "actual code" and it's mostly copy & paste. Notice that if you want to reduce this code the Gradle equivalent code is terse by comparison.
This line needs to go into the properties section:
<manifold.version>2023.1.5</manifold.version>
The dependencies we use are these:
<dependencies>
<dependency>
<groupId>systems.manifold</groupId>
<artifactId>manifold-json-rt</artifactId>
<version>${manifold.version}</version>
</dependency>
The compilation plugin is the boilerplate that weaves Manifold into the bytecode and makes it seamless for us. It’s the last part of the pom setup:
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.0</version>
<configuration>
<source>19</source>
<target>19</target>
<encoding>UTF-8</encoding>
<compilerArgs>
<!-- Configure manifold plugin-->
<arg>-Xplugin:Manifold</arg>
</compilerArgs>
<!-- Add the processor path for the plugin -->
<annotationProcessorPaths>
<path>
<groupId>systems.manifold</groupId>
<artifactId>manifold-json</artifactId>
<version>${manifold.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
With the setup complete, let's dive into the code.
Parsing JSON with Manifold
We place a sample JSON file in the project directory under the resources hierarchy. I placed this file under src/main/resources/com/debugagent/json/Test.json
:
{
"firstName": "Shai",
"surname": "Almog",
"website": "https://debugagent.com/",
"active": true,
"details":[
{"key": "value"}
]
}
In the main class, we refresh the Maven project, and you'll notice a new Test class appears. This class is dynamically created by Manifold based on the JSON file. If you change the JSON and refresh Maven, everything updates seamlessly. It’s important to understand that Manifold isn’t a code generator. It compiles the JSON we just wrote into bytecode.
The Test class comes with several built-in capabilities, such as a type-safe builder API that lets you construct JSON objects using builder methods. You can also generate nested objects and convert the JSON to a string by using the write()
and toJson()
methods.
It means we can now write:
Test test = Test.builder().withFirstName("Someone")
.withSurname("Surname")
.withActive(true)
.withDetails(List.of(
Test.details.detailsItem.builder().
withKey("Value 1").build()
))
.build();
Which will printout the following JSON:
{
"firstName": "Someone",
"surname": "Surname",
"active": true,
"details": [
{
"key": "Value 1"
}
]
}
We can similarly read a JSON file using code such as this:
Test readObject = Test.load().fromJson("""
{
"firstName": "Someone",
"surname": "Surname",
"active": true,
"details": [
{
"key": "Value 1"
}
]
}
""");
Note the use of Java 15 TextBlock
syntax for writing a long string. The load()
method returns an object that includes various APIs for reading the JSON. In this case, it is read from a String
but there are APIs for reading it from a URL, file, etc.
Manifold supports various formats, including CSV, XML, and YAML, allowing you to generate and parse any of these formats without writing any boilerplate code or sacrificing type safety. In order to add that support we will need to add additional dependencies to the pom.xml file:
<dependency>
<groupId>systems.manifold</groupId>
<artifactId>manifold-csv-rt</artifactId>
<version>${manifold.version}</version>
</dependency>
<dependency>
<groupId>systems.manifold</groupId>
<artifactId>manifold-xml-rt</artifactId>
<version>${manifold.version}</version>
</dependency>
<dependency>
<groupId>systems.manifold</groupId>
<artifactId>manifold-yaml-rt</artifactId>
<version>${manifold.version}</version>
</dependency>
</dependencies>
With these additional dependencies, this code will print out the same data as the JSON file... With test.write().toCsv()
the output would be:
"firstName","surname","active","details"
"Someone","Surname","true","[manifold.json.rt.api.DataBindings@71070b9c]"
Notice that the Comma Separated Values (CSV) output doesn’t include hierarchy information. That’s a limitation of the CSV format and not the fault of Manifold.
With test.write().toXml()
the output is familiar and surprisingly concise:
<root_object firstName="Someone" surname="Surname" active="true">
<details key="Value 1"/>
</root_object>
With test.write().toYaml()
we again get a familiar printout:
firstName: Someone
surname: Surname
active: true
details:
- key: Value 1
Working with JSON Schema
Manifold also works seamlessly with JSON schema, allowing you to enforce strict rules and constraints. This is particularly useful when working with dates and enums. Manifold seamlessly creates/updates byte code that adheres to the schema, making it much easier to work with complex JSON data.
This schema is copied and pasted from the Manifold github project:
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "http://example.com/schemas/User.json",
"type": "object",
"definitions": {
"Gender": {
"type": "string",
"enum": ["male", "female"]
}
},
"properties": {
"name": {
"type": "string",
"description": "User's full name.",
"maxLength": 80
},
"email": {
"description": "User's email.",
"type": "string",
"format": "email"
},
"date_of_birth": {
"type": "string",
"description": "Date of uses birth in the one and only date standard: ISO 8601.",
"format": "date"
},
"gender": {
"$ref" : "#/definitions/Gender"
}
},
"required": ["name", "email"]
}
It’s a relatively simple schema but I’d like to turn your attention to several things here. It defines name and email as required. This is why when we try to create a User
object using a builder in Manifold, the build()
method requires both parameters:
User.builder("Name", "email@domain.com")
That is just the start... The schema includes a date. Dates are a painful prospect in JSON, the standardization is poor and fraught with issues. The schema also includes a gender field which is effectively an enum. This is all converted to type-safe semantics using common Java classes such as LocalDate:
User u = User.builder("Name", "email@domain.com")
.withDate_of_birth(LocalDate.of(1999, 10, 11))
.withGender(User.Gender.male)
.build();
That can be made even shorter with static imports but the gist of the idea is clear. JSON is effectively native to Java in Manifold.
The Tip of The Iceberg
Manifold is a powerful and exciting project. It revolutionizes JSON parsing in Java but that’s just one tiny portion of what it can do!
We've only scratched the surface of its capabilities in this post. In the next article, we'll dive deeper into Manifold and explore some additional unexpected features.
Please share your experience and thoughts about Manifold in the comments section. If you have any questions, don't hesitate to ask.
Top comments (4)
Builder pattern and plugin look interesting. How would you compare this to Jackson ObjectMapper?
The goals of manifold are very different. Unlike Jakson you won't need to maintain an object map. Just provide a reference JSON or Schema. This means you have a single source of truth that is very consistent with the API you're working with.
It's a radically different approach that aims for seamless integration. I've yet to work on this with a "real world" project but from playing with it the syntax is fantastic.
I am concerned about edge cases. E.g. weird/inconsistent JSON syntax, etc. This sucks with all JSON/Java frameworks though.
Thanks! The "single source of truth" is really good goal. And the syntax is indeed nice. I think keeping the same option open as Lombok vs Delombok: I can see that the explicit JSON Java classes might still make sense in some apps. Or apps in maintenance mode (when code is read/debugged not written anymore)?
I'm not sure if it's practical in the case of Manifold as the project is so much more vast when compared to Lombok. But it would be an interesting exercise.