BAEL-3325: Programmatic generation of JSON Schemas in Java (#14498)

* BAEL-3325: Programmatic Generation of JSON Schemas in Java

* BAEL-3325: fix typo

* BAEL-3325: rerun baeldung intellij formatter

* BAEL-3325: static imports
This commit is contained in:
Chrys Exaucet 2023-08-22 03:33:14 +00:00 committed by GitHub
parent 684dabd78c
commit 854f240059
14 changed files with 677 additions and 2 deletions

View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>json-2</artifactId>
<version>0.0.1-SNAPSHOT</version>
@ -123,6 +123,26 @@
<artifactId>javax.annotation-api</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>com.github.victools</groupId>
<artifactId>jsonschema-generator</artifactId>
<version>${jsonschema-generator.version}</version>
</dependency>
<dependency>
<groupId>com.github.victools</groupId>
<artifactId>jsonschema-module-jackson</artifactId>
<version>${jsonschema-generator.version}</version>
</dependency>
<dependency>
<groupId>jakarta.validation</groupId>
<artifactId>jakarta.validation-api</artifactId>
<version>${jakarta.validation.version}</version>
</dependency>
<dependency>
<groupId>com.github.victools</groupId>
<artifactId>jsonschema-module-jakarta-validation</artifactId>
<version>${jsonschema-generator.version}</version>
</dependency>
</dependencies>
<build>
@ -162,6 +182,55 @@
</plugin>
</plugins>
</pluginManagement>
<plugins>
<plugin>
<groupId>com.github.victools</groupId>
<artifactId>jsonschema-maven-plugin</artifactId>
<version>${jsonschema-generator.version}</version>
<executions>
<execution>
<goals>
<goal>generate</goal>
</goals>
</execution>
</executions>
<configuration>
<packageNames>
<packageName>com.baeldung.jsonschemageneration.plugin</packageName>
</packageNames>
<classNames>
<className>com.baeldung.jsonschemageneration.plugin.Person</className>
</classNames>
<schemaVersion>DRAFT_2020_12</schemaVersion>
<schemaFilePath>src/main/resources/schemas</schemaFilePath>
<schemaFileName>{1}/{0}.json</schemaFileName>
<failIfNoClassesMatch>true</failIfNoClassesMatch>
<options>
<preset>PLAIN_JSON</preset>
<enabled>
<option>DEFINITIONS_FOR_ALL_OBJECTS</option>
<option>FORBIDDEN_ADDITIONAL_PROPERTIES_BY_DEFAULT</option>
</enabled>
<disabled>SCHEMA_VERSION_INDICATOR</disabled>
</options>
<modules>
<module>
<name>Jackson</name>
<options>
<option>RESPECT_JSONPROPERTY_REQUIRED</option>
</options>
</module>
<module>
<name>JakartaValidation</name>
<options>
<option>NOT_NULLABLE_FIELD_IS_REQUIRED</option>
<option>INCLUDE_PATTERN_EXPRESSIONS</option>
</options>
</module>
</modules>
</configuration>
</plugin>
</plugins>
</build>
<properties>
@ -173,6 +242,8 @@
<jackson-jsonld.version>0.1.1</jackson-jsonld.version>
<hydra-jsonld.version>0.4.2</hydra-jsonld.version>
<jsonld-java.version>0.13.0</jsonld-java.version>
<jsonschema-generator.version>4.31.1</jsonschema-generator.version>
<jakarta.validation.version>3.0.2</jakarta.validation.version>
</properties>
</project>

View File

@ -0,0 +1,70 @@
package com.baeldung.jsonschemageneration.configuration;
import java.sql.Timestamp;
import java.util.Date;
import java.util.UUID;
enum Area {
JAVA("JAVA"), KOTLIN("KOTLIN"), SCALA("SCALA"), LINUX("LINUX");
private final String area;
Area(String area) {
this.area = area;
}
public String getArea() {
return area;
}
}
public class AdvancedArticle {
private UUID id;
private String title;
private String content;
@AllowedTypes({ Timestamp.class, Date.class })
private Object createdAt;
private Area area;
public Area getArea() {
return area;
}
public void setArea(Area area) {
this.area = area;
}
public Object getCreatedAt() {
return createdAt;
}
public void setCreatedAt(Object createdAt) {
this.createdAt = createdAt;
}
public UUID getId() {
return id;
}
public void setId(UUID id) {
this.id = id;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
}

View File

@ -0,0 +1,39 @@
package com.baeldung.jsonschemageneration.configuration;
import com.fasterxml.jackson.databind.JsonNode;
import com.github.victools.jsonschema.generator.Option;
import com.github.victools.jsonschema.generator.OptionPreset;
import com.github.victools.jsonschema.generator.SchemaGenerator;
import com.github.victools.jsonschema.generator.SchemaGeneratorConfig;
import com.github.victools.jsonschema.generator.SchemaGeneratorConfigBuilder;
import com.github.victools.jsonschema.generator.SchemaVersion;
import java.util.Optional;
import java.util.UUID;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class AdvancedConfigurationSchemaGenerator {
public static void main(String[] args) {
SchemaGeneratorConfigBuilder configBuilder = new SchemaGeneratorConfigBuilder(SchemaVersion.DRAFT_2020_12, OptionPreset.PLAIN_JSON);
configBuilder.forFields().withInstanceAttributeOverride((node, field, context) -> node.put("readOnly", field.getDeclaredType().isInstanceOf(UUID.class)));
configBuilder.forFields()
.withTargetTypeOverridesResolver(field -> Optional.ofNullable(field.getAnnotationConsideringFieldAndGetterIfSupported(AllowedTypes.class))
.map(AllowedTypes::value)
.map(Stream::of)
.map(stream -> stream.map(subtype -> field.getContext().resolve(subtype)))
.map(stream -> stream.collect(Collectors.toList()))
.orElse(null));
SchemaGeneratorConfig config = configBuilder.with(Option.EXTRA_OPEN_API_FORMAT_VALUES).build();
SchemaGenerator generator = new SchemaGenerator(config);
JsonNode jsonSchema = generator.generateSchema(AdvancedArticle.class);
System.out.println(jsonSchema.toPrettyString());
}
}

View File

@ -0,0 +1,13 @@
package com.baeldung.jsonschemageneration.configuration;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface AllowedTypes {
Class<?>[] value();
}

View File

@ -0,0 +1,44 @@
package com.baeldung.jsonschemageneration.configuration;
import com.baeldung.jsonschemageneration.recursive.Author;
import com.fasterxml.jackson.databind.JsonNode;
import com.github.victools.jsonschema.generator.Option;
import com.github.victools.jsonschema.generator.OptionPreset;
import com.github.victools.jsonschema.generator.SchemaGenerator;
import com.github.victools.jsonschema.generator.SchemaGeneratorConfig;
import com.github.victools.jsonschema.generator.SchemaGeneratorConfigBuilder;
import com.github.victools.jsonschema.generator.SchemaVersion;
import javax.annotation.Nullable;
import javax.validation.constraints.NotNull;
import java.time.Instant;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class IndividualConfigurationSchemaGenerator {
public static void main(String[] args) {
SchemaGeneratorConfigBuilder configBuilder = new SchemaGeneratorConfigBuilder(SchemaVersion.DRAFT_2020_12, OptionPreset.PLAIN_JSON);
configBuilder.forFields().withRequiredCheck(field -> field.getAnnotationConsideringFieldAndGetter(Nullable.class) == null).withArrayUniqueItemsResolver(scope -> scope.getType().getErasedType() == (List.class) ? true : null);
configBuilder.forMethods().withRequiredCheck(method -> method.getAnnotationConsideringFieldAndGetter(NotNull.class) != null);
configBuilder.forTypesInGeneral()
.withArrayUniqueItemsResolver(scope -> scope.getType().getErasedType() == (List.class) ? true : null)
.withDefaultResolver(scope -> scope.getType().getErasedType() == List.class ? Collections.EMPTY_LIST : null)
.withDefaultResolver(scope -> scope.getType().getErasedType() == Date.class ? Date.from(Instant.now()) : null)
.withEnumResolver(scope -> scope.getType().getErasedType().isEnum() ? Stream.of(scope.getType().getErasedType().getEnumConstants()).map(v -> ((Enum<?>) v).name()).collect(Collectors.toList()) : null);
SchemaGeneratorConfig config = configBuilder.with(Option.EXTRA_OPEN_API_FORMAT_VALUES).build();
SchemaGenerator generator = new SchemaGenerator(config);
JsonNode jsonSchema = generator.generateSchema(Author.class);
System.out.println(jsonSchema.toPrettyString());
}
}

View File

@ -0,0 +1,68 @@
package com.baeldung.jsonschemageneration.modules;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.JsonNode;
import com.github.victools.jsonschema.generator.SchemaGenerator;
import com.github.victools.jsonschema.generator.SchemaGeneratorConfigBuilder;
import com.github.victools.jsonschema.module.jackson.JacksonModule;
import java.util.Date;
import java.util.List;
import java.util.UUID;
import static com.github.victools.jsonschema.generator.Option.EXTRA_OPEN_API_FORMAT_VALUES;
import static com.github.victools.jsonschema.generator.OptionPreset.PLAIN_JSON;
import static com.github.victools.jsonschema.generator.SchemaVersion.DRAFT_2020_12;
import static com.github.victools.jsonschema.module.jackson.JacksonOption.RESPECT_JSONPROPERTY_REQUIRED;
public class JacksonModuleSchemaGenerator {
public static void main(String[] args) {
JacksonModule module = new JacksonModule(RESPECT_JSONPROPERTY_REQUIRED);
SchemaGeneratorConfigBuilder configBuilder = new SchemaGeneratorConfigBuilder(DRAFT_2020_12, PLAIN_JSON).with(module).with(EXTRA_OPEN_API_FORMAT_VALUES);
SchemaGenerator generator = new SchemaGenerator(configBuilder.build());
JsonNode jsonSchema = generator.generateSchema(Person.class);
System.out.println(jsonSchema.toPrettyString());
}
static class Person {
@JsonProperty(access = JsonProperty.Access.READ_ONLY)
UUID id;
@JsonProperty(access = JsonProperty.Access.READ_WRITE, required = true)
String name;
@JsonProperty(access = JsonProperty.Access.READ_WRITE, required = true)
String surname;
@JsonProperty(access = JsonProperty.Access.READ_WRITE, required = true)
Address address;
@JsonIgnore
String fullName;
@JsonProperty(access = JsonProperty.Access.READ_ONLY)
Date createdAt;
@JsonProperty(access = JsonProperty.Access.READ_WRITE)
List<Person> friends;
}
static class Address {
@JsonProperty()
String street;
@JsonProperty(required = true)
String city;
@JsonProperty(required = true)
String country;
}
}

View File

@ -0,0 +1,63 @@
package com.baeldung.jsonschemageneration.modules;
import com.fasterxml.jackson.databind.JsonNode;
import com.github.victools.jsonschema.generator.SchemaGenerator;
import com.github.victools.jsonschema.generator.SchemaGeneratorConfigBuilder;
import com.github.victools.jsonschema.module.jakarta.validation.JakartaValidationModule;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Null;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
import java.util.Date;
import java.util.List;
import java.util.UUID;
import static com.github.victools.jsonschema.generator.OptionPreset.PLAIN_JSON;
import static com.github.victools.jsonschema.generator.SchemaVersion.DRAFT_2020_12;
import static com.github.victools.jsonschema.module.jakarta.validation.JakartaValidationOption.INCLUDE_PATTERN_EXPRESSIONS;
import static com.github.victools.jsonschema.module.jakarta.validation.JakartaValidationOption.NOT_NULLABLE_FIELD_IS_REQUIRED;
public class JakartaValidationModuleSchemaGenerator {
public static void main(String[] args) {
JakartaValidationModule module = new JakartaValidationModule(NOT_NULLABLE_FIELD_IS_REQUIRED, INCLUDE_PATTERN_EXPRESSIONS);
SchemaGeneratorConfigBuilder configBuilder = new SchemaGeneratorConfigBuilder(DRAFT_2020_12, PLAIN_JSON).with(module);
SchemaGenerator generator = new SchemaGenerator(configBuilder.build());
JsonNode jsonSchema = generator.generateSchema(Person.class);
System.out.println(jsonSchema.toPrettyString());
}
static class Person {
@NotNull UUID id;
@NotNull String name;
@NotNull @Email @Pattern(regexp = "\\b[A-Za-z0-9._%+-]+@baeldung\\.com\\b") String email;
@NotNull String surname;
@NotNull Address address;
@Null String fullName;
@NotNull Date createdAt;
@Size(max = 10) List<Person> friends;
}
static class Address {
@Null String street;
@NotNull String city;
@NotNull String country;
}
}

View File

@ -0,0 +1,61 @@
package com.baeldung.jsonschemageneration.plugin;
import com.fasterxml.jackson.annotation.JsonProperty;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Null;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
import java.util.Date;
import java.util.List;
import java.util.UUID;
public class Person {
@NotNull
@JsonProperty(access = JsonProperty.Access.READ_ONLY)
UUID id;
@NotNull
@JsonProperty(access = JsonProperty.Access.READ_WRITE, required = true)
String name;
@NotNull
@JsonProperty(access = JsonProperty.Access.READ_WRITE, required = true)
@Email @Pattern(regexp = "\\b[A-Za-z0-9._%+-]+@baeldung\\.com\\b") String email;
@NotNull
@JsonProperty(access = JsonProperty.Access.READ_WRITE, required = true)
String surname;
@NotNull
@JsonProperty(access = JsonProperty.Access.READ_WRITE, required = true)
Address address;
@Null String fullName;
@JsonProperty(access = JsonProperty.Access.READ_ONLY)
Date createdAt;
@Size(max = 10)
@JsonProperty(access = JsonProperty.Access.READ_WRITE)
List<Person> friends;
}
class Address {
@Null
@JsonProperty
String street;
@NotNull
@JsonProperty(required = true)
String city;
@NotNull
@JsonProperty(required = true)
String country;
}

View File

@ -0,0 +1,12 @@
package com.baeldung.jsonschemageneration.recursive;
import java.util.List;
import java.util.UUID;
public class Author {
private UUID id;
private String name;
private String role;
private List<AuthoredArticle> articles;
}

View File

@ -0,0 +1,68 @@
package com.baeldung.jsonschemageneration.recursive;
import java.util.Date;
import java.util.UUID;
enum Area {
JAVA("JAVA"), KOTLIN("KOTLIN"), SCALA("SCALA"), LINUX("LINUX");
private final String area;
Area(String area) {
this.area = area;
}
public String getArea() {
return area;
}
}
public class AuthoredArticle {
private UUID id;
private String title;
private String content;
private Date createdAt;
private Area area;
private Author author;
public Area getArea() {
return area;
}
public void setArea(Area area) {
this.area = area;
}
public Date getCreatedAt() {
return createdAt;
}
public void setCreatedAt(Date createdAt) {
this.createdAt = createdAt;
}
public UUID getId() {
return id;
}
public void setId(UUID id) {
this.id = id;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
}

View File

@ -0,0 +1,23 @@
package com.baeldung.jsonschemageneration.recursive;
import com.fasterxml.jackson.databind.JsonNode;
import com.github.victools.jsonschema.generator.Option;
import com.github.victools.jsonschema.generator.OptionPreset;
import com.github.victools.jsonschema.generator.SchemaGenerator;
import com.github.victools.jsonschema.generator.SchemaGeneratorConfig;
import com.github.victools.jsonschema.generator.SchemaGeneratorConfigBuilder;
import com.github.victools.jsonschema.generator.SchemaVersion;
public class RecursiveSchemaGenerator {
public static void main(String[] args) {
SchemaGeneratorConfigBuilder configBuilder = new SchemaGeneratorConfigBuilder(SchemaVersion.DRAFT_2020_12, OptionPreset.PLAIN_JSON);
SchemaGeneratorConfig config = configBuilder.with(Option.EXTRA_OPEN_API_FORMAT_VALUES).without(Option.FLATTENED_ENUMS_FROM_TOSTRING).build();
SchemaGenerator generator = new SchemaGenerator(config);
JsonNode jsonSchema = generator.generateSchema(AuthoredArticle.class);
System.out.println(jsonSchema.toPrettyString());
}
}

View File

@ -0,0 +1,66 @@
package com.baeldung.jsonschemageneration.simple;
import java.util.Date;
import java.util.UUID;
enum Area {
JAVA("JAVA"), KOTLIN("KOTLIN"), SCALA("SCALA"), LINUX("LINUX");
private final String area;
Area(String area) {
this.area = area;
}
public String getArea() {
return area;
}
}
public class Article {
private UUID id;
private String title;
private String content;
private Date createdAt;
private Area area;
public Area getArea() {
return area;
}
public void setArea(Area area) {
this.area = area;
}
public Date getCreatedAt() {
return createdAt;
}
public void setCreatedAt(Date createdAt) {
this.createdAt = createdAt;
}
public UUID getId() {
return id;
}
public void setId(UUID id) {
this.id = id;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
}

View File

@ -0,0 +1,23 @@
package com.baeldung.jsonschemageneration.simple;
import com.fasterxml.jackson.databind.JsonNode;
import com.github.victools.jsonschema.generator.Option;
import com.github.victools.jsonschema.generator.OptionPreset;
import com.github.victools.jsonschema.generator.SchemaGenerator;
import com.github.victools.jsonschema.generator.SchemaGeneratorConfig;
import com.github.victools.jsonschema.generator.SchemaGeneratorConfigBuilder;
import com.github.victools.jsonschema.generator.SchemaVersion;
public class SimpleSchemaGenerator {
public static void main(String[] args) {
SchemaGeneratorConfigBuilder configBuilder = new SchemaGeneratorConfigBuilder(SchemaVersion.DRAFT_2020_12, OptionPreset.PLAIN_JSON);
SchemaGeneratorConfig config = configBuilder.with(Option.EXTRA_OPEN_API_FORMAT_VALUES).without(Option.FLATTENED_ENUMS_FROM_TOSTRING).build();
SchemaGenerator generator = new SchemaGenerator(config);
JsonNode jsonSchema = generator.generateSchema(Article.class);
System.out.println(jsonSchema.toPrettyString());
}
}

View File

@ -0,0 +1,54 @@
{
"$defs" : {
"Address" : {
"type" : "object",
"properties" : {
"city" : {
"type" : "string"
},
"country" : {
"type" : "string"
},
"street" : {
"type" : [ "string", "null" ]
}
},
"required" : [ "city", "country" ],
"additionalProperties" : false
}
},
"type" : "object",
"properties" : {
"address" : {
"$ref" : "#/$defs/Address"
},
"createdAt" : {
"type" : "string",
"readOnly" : true
},
"email" : {
"type" : "string",
"format" : "email",
"pattern" : "\\b[A-Za-z0-9._%+-]+@baeldung\\.com\\b"
},
"friends" : {
"maxItems" : 10,
"type" : "array",
"items" : {
"$ref" : "#"
}
},
"id" : {
"type" : "string",
"readOnly" : true
},
"name" : {
"type" : "string"
},
"surname" : {
"type" : "string"
}
},
"required" : [ "address", "email", "id", "name", "surname" ],
"additionalProperties" : false
}