Generate records from Json schema

204 views Asked by At

For many of my projects I like to use the org.jsonschema2pojo.jsonschema2pojo-maven-plugin maven plugin in order to generate some POJO class from a json schema file. This works great and saves me from writing boiler plate code.

However, I often treat these classes as final and immutable so I would like the ability to generate Java records instead so I can enforce those restrictions and get the performance enhancement that comes along with records. I've gone through the documentation and the GitLab repo but I don't see any way to do this with this particular plugin.

Is there something I've missed or is there another plugin or library that offers this ability?

Edit: Assume I've done my research and Java records are in fact what I want/need. I don't want mutability, I don't want extendability, I'm okay with exposing member fields, I want improved performance on comparison operations (.equals), and I want to leverage pattern matching.

1

There are 1 answers

3
Mr. Polywhirl On

I would avoid records, not only because they should only store a few values, but because they do not have the same level of control as a traditional class.

The following article breaks this all down:

https://www.baeldung.com/java-record-vs-lombok

With Jackson's @JsonProperty annotation, you can tell the serializer/deserializer how to map each key to the appropriate field.

If you generate the class stubs and add in some Lombok, you can simplify this process.

The following JSON:

/** https://json.org/example.html */
{
  "glossary": {
    "title": "example glossary",
    "GlossDiv": {
      "title": "S",
      "GlossList": {
        "GlossEntry": {
          "ID": "SGML",
          "SortAs": "SGML",
          "GlossTerm": "Standard Generalized Markup Language",
          "Acronym": "SGML",
          "Abbrev": "ISO 8879:1986",
          "GlossDef": {
            "para": "A meta-markup language, used to create markup languages such as DocBook.",
            "GlossSeeAlso": ["GML", "XML"]
          },
          "GlossSee": "markup"
        }
      }
    }
  }
}

Get converted to the following shapes:

package org.example.json;

import java.io.IOException;
import java.io.InputStream;
import java.util.List;

import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.Data;

public class GenerateClasses {
    private static final ObjectMapper om = new ObjectMapper();
    private static final ClassLoader classLoader = GenerateClasses.class.getClassLoader();

    public static void main(String[] args) {
        try (InputStream is = classLoader.getResourceAsStream("glossary.json")) {
            Root root = om.readValue(is, Root.class);
            System.out.println(root); // Object::toString
            System.out.println(om.writerWithDefaultPrettyPrinter().writeValueAsString(root));
        } catch (IOException e) {
            System.out.println("Failed to read JSON");
        }
    }

    @Data
    public static class Glossary {
        private String title;
        @JsonProperty("GlossDiv")
        private GlossDiv glossDiv;
    }

    @Data
    public static class GlossDef {
        private String para;
        @JsonProperty("GlossSeeAlso")
        private List<String> glossSeeAlso; /* Changed from ArrayList */
    }

    @Data
    public static class GlossDiv {
        private String title;
        @JsonProperty("GlossList")
        private GlossList glossList;
    }

    @Data
    public static class GlossEntry {
        @JsonProperty("ID")
        private String id; /* Manually changed from 'iD' */
        @JsonProperty("SortAs")
        private String sortAs;
        @JsonProperty("GlossTerm")
        private String glossTerm;
        @JsonProperty("Acronym")
        private String acronym;
        @JsonProperty("Abbrev")
        private String abbrev;
        @JsonProperty("GlossDef")
        private GlossDef glossDef;
        @JsonProperty("GlossSee")
        private String glossSee;
    }

    @Data
    public static class GlossList {
        @JsonProperty("GlossEntry")
        private GlossEntry glossEntry;
    }

    @Data
    public static class Root {
        private Glossary glossary;
    }
}

I generated the code above using the following tool:

https://json2csharp.com/code-converters/json-to-pojo (choose JSON > Java)

I added Lombok @Data annotations, made all fields private, and modified fields where noted.


Jackson + Records Caveat

Please note that Jackson versions below 2.11.4 have trouble with deserializing JSON with records:

https://dev.to/brunooliveira/practical-java-16-using-jackson-to-serialize-records-4og4

The following modified program (from above) uses records instead of classes mized with Lombok. It no longer depends on Lombok.

package org.example.json;

import java.io.IOException;
import java.io.InputStream;
import java.util.List;

import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.ObjectMapper;

public class GenerateClasses {
    private static final ObjectMapper om = new ObjectMapper();
    private static final ClassLoader classLoader = GenerateClasses.class.getClassLoader();

    public static void main(String[] args) {
        try (InputStream is = classLoader.getResourceAsStream("glossary.json")) {
            Root root = om.readValue(is, Root.class);
            System.out.println(root); // Object::toString
            System.out.println(om.writerWithDefaultPrettyPrinter().writeValueAsString(root));
        } catch (IOException e) {
            System.out.println("Failed to read JSON");
            System.err.println(e);
        }
    }

    public record Glossary (
        String title,
        @JsonProperty("GlossDiv")
        GlossDiv glossDiv
    ) {}

    public record GlossDef (
        String para,
        @JsonProperty("GlossSeeAlso")
        List<String> glossSeeAlso /* Changed from ArrayList */
    ) {}

    public record GlossDiv (
        String title,
        @JsonProperty("GlossList")
        GlossList glossList
    ) {}

    public record GlossEntry (
        @JsonProperty("ID")
        String id, /* Manually changed from 'iD' */
        @JsonProperty("SortAs")
        String sortAs,
        @JsonProperty("GlossTerm")
        String glossTerm,
        @JsonProperty("Acronym")
        String acronym,
        @JsonProperty("Abbrev")
        String abbrev,
        @JsonProperty("GlossDef")
        GlossDef glossDef,
        @JsonProperty("GlossSee")
        String glossSee
    ) {}

    public record GlossList (
        @JsonProperty("GlossEntry")
        GlossEntry glossEntry
    ) {}

    public record Root (
        Glossary glossary
    ) {}
}

If you encounter the following exception, you will need to update your Jackson dependency:

com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of org.example.json.GenerateClasses$Root (no Creators, like default construct, exist): cannot deserialize from Object value (no delegate- or property-based Creator)
  at [Source: (BufferedInputStream); line: 2, column: 3]