Friends of OpenJDK Today

Revolutionize JSON Parsing in Java with Manifold

May 26, 2023

Author(s)

  • Avatar photo
    Shai Almog

    Author, DevRel, Blogger, Open Source Hacker, Java Rockstar, Conference Speaker, Instructor and Entrepreneur.

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.

Image description

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:

<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 print out 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>

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 protected]")

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 protected]")
       .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.

Topics:

Related Articles

View All

Author(s)

  • Avatar photo
    Shai Almog

    Author, DevRel, Blogger, Open Source Hacker, Java Rockstar, Conference Speaker, Instructor and Entrepreneur.

Comments (0)

Your email address will not be published. Required fields are marked *

Highlight your code snippets using [code lang="language name"] shortcode. Just insert your code between opening and closing tag: [code lang="java"] code [/code]. Or specify another language.

Save my name, email, and website in this browser for the next time I comment.

Subscribe to foojay updates:

https://foojay.io/feed/
Copied to the clipboard