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

Fix generation for swaggers with oneOf construct, objects with null properties and inline schemas #12

Merged
merged 24 commits into from
Jun 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
76e00f6
Prima implementazione fix generazione oneOf
May 28, 2024
a35edc0
Gestione array type object witouth props
May 28, 2024
6ed5f7f
Prima implementazione fix generazione con schema inline
May 28, 2024
880c9d5
Merge branch 'fix/objWithoutProps' into fix/oneOfGeneration
May 28, 2024
da7ae07
Add .iml a gitignore
May 28, 2024
8e0c4fc
Implementazione test per oggetti con properties vuote
May 28, 2024
06ac8da
forcing additionalProps to true in case of type:object without props
May 28, 2024
54cf337
Merge commit '06ac8da2fcde59ae5f205ce14d1aef8e74afe81e' into fix/oneO…
May 28, 2024
08fefb7
Aggiornamento swagger di test
May 28, 2024
016f7d8
fix forcing additionalProperties to true with grafted objs
May 28, 2024
d9e2d18
Merge commit '016f7d8c46b815b615454c51838bec6be3338184' into fix/oneO…
May 28, 2024
3082663
Add .iml a gitignore
May 29, 2024
dbbaf4b
Implementazione test per generazione con properties vuote
May 29, 2024
046296c
Fix swagger di test
May 29, 2024
a76cb6a
Merge branch 'fix/objWithoutProps' into develop
May 29, 2024
9d87d8f
Merge branch 'fix/oneOfGeneration' into develop
May 29, 2024
eeeb0a7
Update changelog per merge feature
May 29, 2024
46d0963
Fix nomi json schema generati
May 29, 2024
8f1df52
Merge branch 'fix/schemaInOperation' into develop
May 29, 2024
a740639
Fix errori di generazione in caso di swagger con schema inline
May 30, 2024
af24f49
Aggiunta voci changelog
May 30, 2024
97ef060
Fix date changelog
May 30, 2024
1231132
Fix mancato uso della clausola "strict" per additionalProperties
May 31, 2024
0ace175
Delete openapi2jsonschema4j.iml
gcornacchia Jun 7, 2024
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
*.idea*
*.iml
target
*.iml
*.log
*.log*.zip
.classpath
Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# CHANGELOG

1.0.7 - 30/05/2024
- fix bug while handling generation of oneOf construct inside json schema
- fix issue preventing generation of schemas in case of objects without properties, forcing generation with additionalProperties enabled
- fix issue preventing generation of json schemas with swagger files that have objects schemas defined inline rather than inside "components"

1.0.6 - 07/07/2023
- updated swagger-parser library due to a bug that could not read additionalProperties inside a model.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.Operation;
import io.swagger.v3.oas.models.PathItem;
import io.swagger.v3.oas.models.media.ObjectSchema;
import io.swagger.v3.oas.models.media.ArraySchema;
import io.swagger.v3.oas.models.media.Schema;
import io.swagger.v3.oas.models.responses.ApiResponse;
import io.swagger.v3.parser.core.models.ParseOptions;
Expand Down Expand Up @@ -50,7 +52,9 @@ protected void readFromInterface(File interfaceFile) {
SwaggerParseResult result = new OpenAPIParser().readLocation(interfaceFile.getAbsolutePath(),null,po);
OpenAPI swagger = result.getOpenAPI();
Validate.notNull(swagger,"Error during parsing of interface file "+interfaceFile.getAbsolutePath());
objectsDefinitions = swagger.getComponents().getSchemas();
if (swagger.getComponents() != null && swagger.getComponents().getSchemas() != null) {
objectsDefinitions = swagger.getComponents().getSchemas();
}
for (Map.Entry<String, PathItem> entry : swagger.getPaths().entrySet()) {
String k = entry.getKey();
PathItem v = entry.getValue();
Expand All @@ -66,11 +70,17 @@ private void analyzeOperation(PathItem v) {
ApiResponse r = op.getResponses().get(key);
if (r.getContent()!=null) {
if (r.getContent().get(APPLICATION_JSON) != null) {
Schema sc = r.getContent().get(APPLICATION_JSON).getSchema();
if (r.getContent().get(APPLICATION_JSON).getSchema().get$ref() != null) {
log.info("code={} responseSchema={}", key, r.getContent().get(APPLICATION_JSON).getSchema().get$ref());
messageObjects.add(r.getContent().get(APPLICATION_JSON).getSchema().get$ref());
} else {
log.warn("code={} response schema is not a referenced definition! type={}", key, r.getContent().get("application/json").getClass());
log.debug("Reference not found, creating it manually");
if (!(sc instanceof ArraySchema)) {
objectsDefinitions.put(op.getOperationId()+"response"+key, sc);
messageObjects.add(op.getOperationId()+"response"+key);
}
}
}
}
Expand All @@ -85,9 +95,14 @@ private void findRequestBodySchema(Operation op, Set<String> messageObjects) {
if (sc != null) {
log.info("Request schema={}", sc.get$ref());
if (sc.get$ref()!=null) {
messageObjects.add(sc.get$ref());
messageObjects.add(sc.get$ref());
} else {
log.warn("Request schema is not a referenced definition!");
log.debug("Ref not found, cresting it manually if object");
if (!(sc instanceof ArraySchema)) {
objectsDefinitions.put(op.getOperationId()+"request", sc);
messageObjects.add(op.getOperationId()+"request");
}
}
}
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.datatype.jsr310.*;

import io.swagger.v3.oas.models.Operation;
import io.swagger.v3.oas.models.media.*;

import com.fasterxml.jackson.annotation.JsonInclude.Include;
Expand Down Expand Up @@ -39,6 +41,11 @@ public class DraftV4JsonSchemaGenerator extends BaseJsonSchemaGenerator implemen
private static final String EXTERNALDOCS = "externalDocs";
private static final String DEPRECATED = "deprecated";

private static final String ALLOF = "anyOf";
private static final String ONEOF = "oneOf";
private static final String ANYOF = "anyOf";


private static final String JSONSCHEMA = "jsonSchema";

private static final String TYPES = "types";
Expand All @@ -59,35 +66,53 @@ public class DraftV4JsonSchemaGenerator extends BaseJsonSchemaGenerator implemen
public static final String NULL = "null";
private boolean strict;


public DraftV4JsonSchemaGenerator(boolean strict) {
this.strict = strict;
}

private Map<String, JsonNode> generateForObjects() throws Exception {
for (String ref : getMessageObjects()) {
String title = ref.replace(DEFINITIONS2, "");
Map<String, Object> defs = (Map<String, Object>) ((HashMap<String, Schema>) getObjectsDefinitions()).clone();
Map<String, Object> defs = (Map<String, Object>) ((HashMap<String, Schema>) getObjectsDefinitions()).clone();
Schema<Object> ob = (Schema<Object>) defs.get(title);
defs.remove(title);
Map<String, Object> res = new HashMap<String, Object>();
Map<String,Object> schemas = new HashMap<>();
schemas.put(SCHEMAS,defs);
res.put(COMPONENTS, schemas);
res.put(TITLE2, title);
res.put(TITLE2, title);
log.info("Generating json schema for object '{}' of type {}", title,ob.getClass());
if (ob instanceof ObjectSchema) {
res.put(TYPE, ((ObjectSchema) ob).getType());
res.put(PROPERTIES, ob.getProperties());
res.put(REQUIRED,ob.getRequired());
if (((ObjectSchema) ob).getAdditionalProperties()!=null) {
log.info("additionalProperties already exists... {}",((ObjectSchema) ob).getAdditionalProperties());
res.put(ADDITIONAL_PROPERTIES,((ObjectSchema) ob).getAdditionalProperties());
res.put(REQUIRED, ob.getRequired());

if (ob.getProperties() == null || ob.getProperties().isEmpty()) {
res.put(PROPERTIES, new HashMap<String, Schema>());
log.info("Object '{}' has no properties, creating empty properties object.");
res.put(ADDITIONAL_PROPERTIES, true);
log.info("FORCED ADDITIONALPROPERTIES TO TRUE");
} else {
res.put(ADDITIONAL_PROPERTIES, !this.strict);
if (((ObjectSchema) ob).getAdditionalProperties()!=null) {
log.info("additionalProperties already exists... {}",((ObjectSchema) ob).getAdditionalProperties());
res.put(ADDITIONAL_PROPERTIES,((ObjectSchema) ob).getAdditionalProperties());
} else {
res.put(ADDITIONAL_PROPERTIES, !this.strict);
}
}
}
if (ob instanceof ComposedSchema) {
res.put(TYPE, ((ComposedSchema) ob).getType());
res.put(ALLOF, ob.getAllOf());
res.put(ONEOF, ob.getOneOf());
res.put(ANYOF, ob.getAnyOf());
}
if (ob instanceof ArraySchema) {
Schema<?> items = ob.getItems();
if (items instanceof ObjectSchema && items.getProperties() == null) {
log.info("Array items of type object has no properties");
}
res.put(ITEMS, ((ArraySchema) ob).getItems());
res.put(TYPE, ((ArraySchema) ob).getType());
res.put(MIN_ITEMS, ((ArraySchema) ob).getMinItems());
Expand All @@ -96,15 +121,14 @@ private Map<String, JsonNode> generateForObjects() throws Exception {
if (ob instanceof MapSchema) {
res.put(PROPERTIES, ((MapSchema) ob).getProperties());
res.put(TYPE, ((MapSchema) ob).getType());
res.put(REQUIRED,ob.getRequired());
res.put(ADDITIONAL_PROPERTIES,((MapSchema) ob).getAdditionalProperties());
res.put(REQUIRED, ob.getRequired());
res.put(ADDITIONAL_PROPERTIES, ((MapSchema) ob).getAdditionalProperties());
}
res.put($SCHEMA, HTTP_JSON_SCHEMA_ORG_DRAFT_04_SCHEMA);
removeUnusedObject(res,ob);
getGeneratedObjects().put(title, postprocess(res));
}
return getGeneratedObjects();

}

private void removeUnusedObject(Map<String, Object> res, Schema<Object> ob) {
Expand Down Expand Up @@ -173,8 +197,8 @@ private void navigateModel(String originalRef, List<String> usedDefinition, Map<
if (ms.getAdditionalProperties() instanceof Schema) {
navigateSchema("", (Schema)ms.getAdditionalProperties(), usedDefinition, res);
}
} else if (ob instanceof Schema && ((Schema)ob).get$ref()!=null){

} else if (ob instanceof Schema && ((Schema)ob).get$ref()!=null){
navigateModel(((Schema)ob).get$ref(),usedDefinition,res,null);
}
}
Expand All @@ -194,11 +218,16 @@ private void navigateSchema(String propertyName, Schema p, List<String> usedDefi
} else if (p instanceof ArraySchema) {
ArraySchema ap = (ArraySchema) p;
log.debug("Array property={} items={}",ap,ap.getItems());
if(ap.getItems() instanceof ObjectSchema && ap.getItems().getProperties() == null ){
log.info("Array items of type object has no properties");
}
navigateSchema("items",ap.getItems(),usedDefinition,res);
} else if (p instanceof ObjectSchema){
ObjectSchema op = (ObjectSchema) p;
for (String name : op.getProperties().keySet()){
navigateSchema(name,op.getProperties().get(name),usedDefinition,res);
if(op.getProperties() != null) {
for (String name : op.getProperties().keySet()) {
navigateSchema(name, op.getProperties().get(name), usedDefinition, res);
}
}
} else if (p instanceof MapSchema) {
MapSchema mp = (MapSchema)p;
Expand All @@ -216,7 +245,7 @@ public class DynamicMixIn {
}

private JsonNode postprocess(Map<String, Object> res) throws Exception {
//devo gestire i valori nullable potenzialmente presenti su oas 3.0
//need to handle all nullable oas3 possible values
res = handleNullableFields(res);
JsonNode jsonNode = removeNonJsonSchemaProperties(res);
process("", jsonNode);
Expand All @@ -229,7 +258,7 @@ private JsonNode postprocess(Map<String, Object> res) throws Exception {
}

private JsonNode removeNonJsonSchemaProperties(Map<String, Object> res) throws JsonProcessingException {

iterateMap(res,null);
ObjectMapper mapper = new ObjectMapper();
mapper.setSerializationInclusion(Include.NON_NULL);
Expand All @@ -238,14 +267,14 @@ private JsonNode removeNonJsonSchemaProperties(Map<String, Object> res) throws J
JsonNode jsonNode = mapper2.readValue(json, JsonNode.class);
return jsonNode;
}
//rimuove tutte le properties di oas3 non gestite in json schema

//this method remove all unmanaged oas3 json schema props
private void iterateMap(Map<String, Object> res, String father) {
if (res==null)
return;
for (String k : res.keySet()) {
if (res.get(k)!=null) {
log.debug("key={}",k);
log.debug("key={} father={}",k,father);
if (!"properties".equals(father)) {
//devo rimuovere i valori da ignorare (solo se il padre non è un campo 'properties'
if (ignorePropertiesList.contains(k)) {
Expand All @@ -259,11 +288,11 @@ private void iterateMap(Map<String, Object> res, String father) {
}
}
}

}




private Map<String, Object> handleNullableFields(Map<String, Object> result) {
ObjectMapper mapper = new ObjectMapper();
Expand All @@ -286,33 +315,34 @@ private void process(String prefix, JsonNode currentNode) {
currentNode.fields().forEachRemaining(entry -> process(
!prefix.isEmpty() ? prefix + "-" + entry.getKey() : entry.getKey(), entry.getValue()));
ObjectNode on = ((ObjectNode) currentNode);
if (currentNode.get(TYPE) != null) {
String type = currentNode.get(TYPE).asText();
if ("object".equals(type)) {
if (on.get(ADDITIONAL_PROPERTIES)!=null) {
log.debug("already defined additionalProperties with value {}",on.get(ADDITIONAL_PROPERTIES).asText());
} else {
if (currentNode.get(PROPERTIES)!=null && currentNode.get(PROPERTIES).isEmpty()) {
//devo settare additionalProperties a true come di default se l'oggetto non specifica nessuna property
on.set(ADDITIONAL_PROPERTIES, BooleanNode.valueOf(true));
log.debug("setting additional properties with value {} as this object has empty properties", true);
} else {
on.set(ADDITIONAL_PROPERTIES, BooleanNode.valueOf(!this.strict));
log.debug("setting additional properties with value {}", !this.strict);
}
}
if (currentNode.has(TYPE) && "object".equals(currentNode.get(TYPE).asText())) {
if (on.get(ADDITIONAL_PROPERTIES) != null) {
log.debug("already defined additionalProperties with value {}",on.get(ADDITIONAL_PROPERTIES).asText());
}
if (!currentNode.has(PROPERTIES) || !currentNode.get(PROPERTIES).fields().hasNext()) {
on.put(ADDITIONAL_PROPERTIES, true);
log.debug("setting additional properties with value {}", true);
}else{
on.set(ADDITIONAL_PROPERTIES, BooleanNode.valueOf(!this.strict));
log.debug("setting additional properties with value {}", !this.strict);
}
}
if (currentNode.get(ORIGINAL_REF) != null) {
if (currentNode.has(ORIGINAL_REF)) {
on.remove(ORIGINAL_REF);
log.debug("removing originalRef field");
}
if (currentNode.get(EXAMPLESETFLAG) != null) {
on.remove(EXAMPLESETFLAG);
log.debug("removing exampleSetFlag field");
}
} else {
log.debug(prefix + ": " + currentNode.toString());
}
}




private boolean isValidJsonSchemaSyntax(JsonNode jsonSchemaFile) {
SyntaxValidator synValidator = JsonSchemaFactory.byDefault().getSyntaxValidator();
ProcessingReport report = synValidator.validateSchema(jsonSchemaFile);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package it.imolainformatica.openapi2jsonschema4j.base;
import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

import com.fasterxml.jackson.databind.JsonNode;
Expand Down Expand Up @@ -61,7 +62,10 @@ private void testGeneratedJsonSchema(File f, Map<String, JsonNode> gen) throws P
SwaggerParseResult result = new OpenAPIParser().readLocation(f.getAbsolutePath(),null,parseOptions);
OpenAPI swagger = result.getOpenAPI();

Map<String, Schema> definitions = swagger.getComponents().getSchemas();
Map<String, Schema> definitions = new HashMap<String,Schema>();
if (swagger.getComponents() != null && swagger.getComponents().getSchemas() != null) {
definitions = swagger.getComponents().getSchemas();
}

for (Map.Entry<String, JsonNode> entry : gen.entrySet()) {
String objName = entry.getKey();
Expand All @@ -76,15 +80,14 @@ private void testGeneratedJsonSchema(File f, Map<String, JsonNode> gen) throws P
JsonSchemaFactory schemaFactory = JsonSchemaFactory.byDefault();
JsonSchema jsonSchema = schemaFactory.getJsonSchema(jsonSchemaNode);
validateJsonSchemaSyntax(schemaFactory.getSyntaxValidator(), jsonSchemaNode);
ProcessingReport rep = jsonSchema.validate(new ObjectMapper().readTree(jsonExample));
log.info("processing report for model {} = {}",objName,rep);
Assert.assertTrue("Il json generato non è valido in base al json schema per l'oggetto "+objName, rep.isSuccess());

if (jsonExample.equals(null)) {
log.info("JsonExample is valid, proceeding with validation");
//Validation on example is possible only if components are defined inside the swagger file
ProcessingReport rep = jsonSchema.validate(new ObjectMapper().readTree(jsonExample));
log.info("processing report for model {} = {}",objName,rep);
Assert.assertTrue("Il json generato non è valido in base al json schema per l'oggetto "+objName, rep.isSuccess());
}
}




}

private void validateJsonSchemaSyntax(SyntaxValidator syntaxValidator, JsonNode node) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

@Slf4j
public class TestGeneration extends AbstractIT{

@Test
public void testPetStoreWithStrict() {
testForSwagger("petstore.json");
Expand All @@ -31,5 +31,4 @@ public void testSwaggerWithAdditionalProperties() {
testForSwagger("petstoreAdditionalProperties.json");
}


}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ public class TestGenerationFromOAS3 extends AbstractIT {
@Test
public void testOAS3() { testForSwagger("petstoreoas3.json"); }


@Test
public void testOAS3WithRemoteReferences() { testForSwagger("petstoreoas3-remoteref.json"); }

Expand All @@ -18,5 +17,12 @@ public class TestGenerationFromOAS3 extends AbstractIT {
@Test
public void testOAS3WithAdditionalPropertiesFalse() { testForSwagger("testOASAdditionalPropertiesFalse.json"); }

@Test
public void testOAS3WithOneOf() { testForSwagger("petstoreoas3Oneof.json");}

@Test
public void testOAS3WithObjectTypeNull() { testForSwagger("petstoreoas3ObjectTypeNull.json");}

@Test
public void testOAS3WithComponentsInline() { testForSwagger("petstoreoas3ObjectInline.json"); }
}
Loading
Loading