Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Generate POJOs from subschemas defined in 'definitions'/'$defs' section #1523

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,11 @@ public class Jsonschema2PojoTask extends Task implements GenerationConfig {
private boolean includeGeneratedAnnotation = true;

private boolean useJakartaValidation = false;

private boolean generateDefinitions = false;

private String definitionsPath = "/$defs";

/**
* Execute this task (it's expected that all relevant setters will have been
* called by Ant to provide task configuration <em>before</em> this method
Expand Down Expand Up @@ -977,6 +982,24 @@ public void setUseJakartaValidation(boolean useJakartaValidation) {
this.useJakartaValidation = useJakartaValidation;
}

/**
* Sets the 'generateDefinitions' property of this class
*
* @param generateDefinitions Whether to generate POJO's from subschemas path defined by {@link #getDefinitionsPath()} configuration option
*/
public void setGenerateDefinitions(boolean generateDefinitions) {
this.generateDefinitions = generateDefinitions;
}

/**
* Sets the 'definitionsPath' property of this class
*
* @param definitionsPath Path to subschemas that should be processed by jsonschema2pojo when {@link #isGenerateDefinitions()} is enabled
*/
public void setDefinitionsPath(String definitionsPath) {
this.definitionsPath = definitionsPath;
}

public void setFormatTypeMapping(Map<String, String> formatTypeMapping) {
this.formatTypeMapping = formatTypeMapping;
}
Expand Down Expand Up @@ -1338,4 +1361,14 @@ public boolean isIncludeGeneratedAnnotation() {
public boolean isUseJakartaValidation() {
return useJakartaValidation;
}

@Override
public boolean isGenerateDefinitions() {
return generateDefinitions;
}

@Override
public String getDefinitionsPath() {
return definitionsPath;
}
}
15 changes: 15 additions & 0 deletions jsonschema2pojo-ant/src/site/Jsonschema2PojoTask.html
Original file line number Diff line number Diff line change
Expand Up @@ -632,6 +632,21 @@ <h3>Parameters</h3>
</td>
<td align="center" valign="top">No (default <code>false</code>)</td>
</tr>
<tr>
<td valign="top"><a id="generateDefinitions"></a>generateDefinitions</td>
<td valign="top">Whether to generate POJO's from subschemas path defined by
<a href="#definitionsPath">definitionsPath</a> configuration option
</td>
<td align="center" valign="top">No (default <code>false</code>)</td>
</tr>
<tr>
<td valign="top"><a id="definitionsPath"></a>definitionsPath</td>
<td valign="top">Defines path to subschemas that should be processed by jsonschema2pojo.<br/>
This property works in collaboration with the <a href="#generateDefinitions">generateDefinitions</a> configuration option and
will have no effect if latter is set to <code>false</code>
</td>
<td align="center" valign="top">No (default <code>/$defs</code>)</td>
</tr>
</table>
<h3>Examples</h3>
<pre>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,12 @@ public class Arguments implements GenerationConfig {
@Parameter(names = { "--useJakartaValidation" }, description = "Whether to use annotations from jakarta.validation package instead of javax.validation package when adding JSR-303/349 annotations to generated Java types")
private boolean useJakartaValidation = false;

@Parameter(names = { "--generateDefinitions" }, description = "Whether to generate POJO's from subschemas path defined by '--definitionsPath' configuration option")
private boolean generateDefinitions = false;

@Parameter(names = { "--definitionsPath" }, description = "Defines path to subschemas that should be processed by jsonschema2pojo when '--generateDefinitions' is enabled")
private String definitionsPath = "/$defs";

@Parameter(names = { "-v", "--version"}, description = "Print version information", help = true)
private boolean printVersion = false;

Expand Down Expand Up @@ -624,4 +630,14 @@ public boolean isIncludeGeneratedAnnotation() {
public boolean isUseJakartaValidation() {
return useJakartaValidation;
}

@Override
public boolean isGenerateDefinitions() {
return generateDefinitions;
}

@Override
public String getDefinitionsPath() {
return definitionsPath;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -635,4 +635,24 @@ default boolean isUseInnerClassBuilders() {
*/
boolean isUseJakartaValidation();

/**
* Gets the 'generateDefinitions' configuration option.
*
* @return whether to generate POJO's from subschemas path defined by {@link #getDefinitionsPath} configuration option
*/
default boolean isGenerateDefinitions() {
return false;
}

/**
* Gets the 'definitionsPath' configuration option.
* This property works in collaboration with the {@link #isGenerateDefinitions()} configuration option.
* If the {@link #isGenerateDefinitions()} returns {@code false}, then this configuration option will not affect anything.
*
* @return path to subschemas that should be processed by jsonschema2pojo
*/
default String getDefinitionsPath() {
return "/$defs";
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@

import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.Iterator;
import java.util.Map;

import org.apache.commons.lang3.StringUtils;
import org.jsonschema2pojo.Jsonschema2Pojo;
import org.jsonschema2pojo.Schema;
import org.jsonschema2pojo.exception.GenerationException;
Expand Down Expand Up @@ -84,9 +87,39 @@ public JType apply(String nodeName, JsonNode schemaNode, JsonNode parent, JClass
}
schema.setJavaTypeIfEmpty(javaType);

processDefinitions(schemaNode, generatableType, schema);

return javaType;
}

private void processDefinitions(JsonNode schemaNode, JClassContainer generatableType, Schema parent) {
if (!ruleFactory.getGenerationConfig().isGenerateDefinitions()) {
return;
}

final String definitionsNodePath = getDefinitionsNodePath(schemaNode);
if (StringUtils.isNotBlank(definitionsNodePath)) {
final Iterator<Map.Entry<String, JsonNode>> definitions = schemaNode.at(definitionsNodePath).fields();
while (definitions.hasNext()) {
final Map.Entry<String, JsonNode> definition = definitions.next();

final Schema schema = ruleFactory.getSchemaStore().create(
parent,
"#" + definitionsNodePath + "/" + definition.getKey(),
ruleFactory.getGenerationConfig().getRefFragmentPathDelimiters());
if (schema.isGenerated()) {
continue;
}

apply(definition.getKey(), definition.getValue(), schemaNode, generatableType, schema);
}
}
}

protected String getDefinitionsNodePath(JsonNode schemaNode) {
return ruleFactory.getGenerationConfig().getDefinitionsPath();
}

private String nameFromRef(String ref) {

if ("#".equals(ref)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import java.net.URI;
import java.net.URISyntaxException;

import org.jsonschema2pojo.DefaultGenerationConfig;
import org.jsonschema2pojo.GenerationConfig;
import org.jsonschema2pojo.Schema;
import org.jsonschema2pojo.SchemaStore;
Expand Down Expand Up @@ -92,6 +93,7 @@ public void enumAsRootIsGeneratedCorrectly() throws JClassAlreadyExistsException

EnumRule enumRule = mock(EnumRule.class);
when(mockRuleFactory.getEnumRule()).thenReturn(enumRule);
when(mockRuleFactory.getGenerationConfig()).thenReturn(new DefaultGenerationConfig());

when(enumRule.apply(NODE_NAME, enumNode, null, jclass, schema)).thenReturn(jclass);

Expand Down
6 changes: 6 additions & 0 deletions jsonschema2pojo-gradle-plugin/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,12 @@ jsonSchema2Pojo {
// Whether to use annotations from jakarta.validation package instead of javax.validation package
// when adding JSR-303 annotations to generated Java types
useJakartaValidation = false

// Whether to generate POJO's from subschemas path defined by 'definitionsPath' configuration option
generateDefinitions = false

// Defines path to subschemas that should be processed by jsonschema2pojo when 'generateDefinitions' is enabled
definitionsPath = '/$defs'
}
```

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,8 @@ public class JsonSchemaExtension implements GenerationConfig {
Map<String, String> formatTypeMapping
boolean includeGeneratedAnnotation
boolean useJakartaValidation
boolean generateDefinitions
String definitionsPath

public JsonSchemaExtension() {
// See DefaultGenerationConfig
Expand Down Expand Up @@ -158,6 +160,8 @@ public class JsonSchemaExtension implements GenerationConfig {
formatTypeMapping = Collections.emptyMap()
includeGeneratedAnnotation = true
useJakartaValidation = false
generateDefinitions = false
definitionsPath = '/$defs'
}

@Override
Expand Down Expand Up @@ -292,6 +296,8 @@ public class JsonSchemaExtension implements GenerationConfig {
|includeConstructorPropertiesAnnotation = ${includeConstructorPropertiesAnnotation}
|includeGeneratedAnnotation = ${includeGeneratedAnnotation}
|useJakartaValidation = ${useJakartaValidation}
|generateDefinitions = ${generateDefinitions}
|definitionsPath = ${definitionsPath}
""".stripMargin()
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/**
* Copyright © 2010-2020 Nokia
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.jsonschema2pojo.integration;

import org.jsonschema2pojo.integration.util.Jsonschema2PojoRule;
import org.junit.Rule;
import org.junit.Test;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.emptyArray;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.notNullValue;
import static org.jsonschema2pojo.integration.util.CodeGenerationHelper.config;
import static org.junit.Assert.assertThrows;

public class SchemaIT {

@Rule
public Jsonschema2PojoRule schemaRule = new Jsonschema2PojoRule();

@Test
public void subschemasDefinedInDefsAreGenerated_when_generateDefinitions_isTrue() throws ReflectiveOperationException {
ClassLoader resultsClassLoader = schemaRule.generateAndCompile(
"/schema/definitions/schemaDefsStorage.json",
"com.example",
config("generateDefinitions", true));

Class<?> schemaDefsStorageType = resultsClassLoader.loadClass("com.example.SchemaDefsStorage");
Class<?> referencedDefinitionsStorageType = resultsClassLoader.loadClass("com.example.ReferencedDefsStorage");

assertThat(schemaDefsStorageType.getDeclaredFields(), is(emptyArray()));
assertThat(referencedDefinitionsStorageType.getDeclaredField("name"), is(notNullValue()));
assertInlinePropertyTypes(resultsClassLoader);
}

@Test
public void subschemasNotProcessed_when_schemaDoesNotContainOverridenDefinitionsPath() throws ReflectiveOperationException {
ClassLoader resultsClassLoader = schemaRule.generateAndCompile(
"/schema/definitions/schemaDefsStorage.json",
"com.example",
config("generateDefinitions", true, "definitionsPath", "/components/schemas"));

Class<?> schemaDefsStorageType = resultsClassLoader.loadClass("com.example.SchemaDefsStorage");
assertThat(schemaDefsStorageType.getDeclaredFields(), is(emptyArray()));
assertThrows(ClassNotFoundException.class, () -> resultsClassLoader.loadClass("com.example.ReferencedDefsStorage"));
}

@Test
public void definitionsFromCustomPathProcessed_when_definitionsNodePathIsOverridden() throws ReflectiveOperationException {
ClassLoader resultsClassLoader = schemaRule.generateAndCompile(
"/schema/definitions/schemaDefinitionsStorageWithCustomSection.json",
"com.example",
config("generateDefinitions", true, "definitionsPath", "/components/schemas"));

Class<?> statusTypeRaw = resultsClassLoader.loadClass("com.example.Status");
assertThat(statusTypeRaw.isEnum(), is(true));
@SuppressWarnings("unchecked")
Class<Enum<?>> statusType = (Class<Enum<?>>) statusTypeRaw;
assertThat(statusType.getEnumConstants()[0].name(), is("ACTIVE"));
assertThat(statusType.getEnumConstants()[1].name(), is("INACTIVE"));

Class<?> userType = resultsClassLoader.loadClass("com.example.User");
assertThat(userType.getDeclaredField("id"), is(notNullValue()));
assertThat(userType.getDeclaredField("id").getType(), is(equalTo(Integer.class)));
assertThat(userType.getDeclaredField("name"), is(notNullValue()));
assertThat(userType.getDeclaredField("name").getType(), is(equalTo(String.class)));

assertThrows(ClassNotFoundException.class, () -> resultsClassLoader.loadClass("com.example.Unexpected"));
}

private void assertInlinePropertyTypes(ClassLoader resultsClassLoader) throws ReflectiveOperationException {
Class<?> referencedInlineType = resultsClassLoader.loadClass("com.example.Inline");
Class<?> inlineType = resultsClassLoader.loadClass("com.example.Inline__1");

assertThat(inlineType, is(not(equalTo(referencedInlineType))));

assertThat(referencedInlineType.getDeclaredField("inlineProperty"), is(notNullValue()));
assertThat(referencedInlineType.getDeclaredField("inlineProperty").getType(), is(equalTo(Boolean.class)));

assertThat(inlineType.getDeclaredField("inlineProperty"), is(notNullValue()));
assertThat(inlineType.getDeclaredField("inlineProperty").getType(), is(equalTo(String.class)));

Class<?> selfReferenceType = resultsClassLoader.loadClass("com.example.SelfReference");
assertThat(selfReferenceType.getDeclaredField("selfRefProperty"), is(notNullValue()));
assertThat(selfReferenceType.getDeclaredField("selfRefProperty").getType(), is(equalTo(inlineType)));
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"type": "object",
"additionalProperties": false,
"properties": {
"name": {
"type": "string"
}
},
"$defs": {
"inline": {
"type": "object",
"additionalProperties": false,
"properties": {
"inlineProperty": {
"type": "boolean"
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"definitions": {
"unexpected": {
"type": "object"
}
},
"components": {
"schemas": {
"user": {
"type": "object",
"properties": {
"id": {
"type": "integer"
},
"name": {
"type": "string"
}
}
},
"status": {
"type": "string",
"enum": [
"active",
"inactive"
]
}
}
}
}
Loading
Loading