OpenApi - Is there a way to have a ComposedSchema with a discriminator part in a contract generated with springdoc-openapi-maven-plugin? - spring-boot

I have a sample SpringBoot API with the following features:
1 controller that exposes a single endpoint invokable with a GET request and that returns a custom class (ContainerClass in my example)
ContainerClass contains a property List
ParentClass is an abstract class that has 2 sub-classes: ChildA and ChildB
I try to generate an OpenApi contract from this API with springdoc-openapi-maven-plugin.
In my pom.xml, I have the following elements:
SpringBoot version: 2.2.6
org.springdoc:springdoc-openapi-ui:1.4.1
org.springdoc:springdoc-openapi-maven-plugin:1.0
Here are my classes I generate schema from.
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Schema;
public class ContainerClass {
#ArraySchema(
arraySchema = #Schema(discriminatorProperty = "classType"),
schema = #Schema(implementation = ParentClass.class)
)
public List<ParentClass> elements;
// + Getter/Setter
}
#JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.EXISTING_PROPERTY,
property = "classType",
defaultImpl = ParentClass.class,
visible = true)
#JsonSubTypes({
#JsonSubTypes.Type(value = ChildA.class, name = "CHILD_A"),
#JsonSubTypes.Type(value = ChildB.class, name = "CHILD_B")})
#Schema(
description = "Parent description",
discriminatorProperty = "classType",
discriminatorMapping = {
#DiscriminatorMapping(value = "CHILD_A", schema = ChildA.class),
#DiscriminatorMapping(value = "CHILD_B", schema = ChildB.class)
}
)
public abstract class ParentClass {
public String classType;
// + Getter/Setter
}
#io.swagger.v3.oas.annotations.media.Schema(description = " Child A", allOf = ParentClass.class)
public class ChildA extends ParentClass{
}
#io.swagger.v3.oas.annotations.media.Schema(description = " Child B", allOf = ParentClass.class)
public class ChildB extends ParentClass{
}
When I run springdoc-openapi-maven-plugin, I get the following contract file.
openapi: 3.0.1
info:
title: OpenAPI definition
version: v0
servers:
- url: http://localhost:8080
description: Generated server url
paths:
/container:
get:
tags:
- hello-controller
operationId: listElements
responses:
"200":
description: OK
content:
'*/*':
schema:
$ref: '#/components/schemas/ContainerClass'
components:
schemas:
ChildA:
type: object
description: ' Child A'
allOf:
- $ref: '#/components/schemas/ParentClass'
ChildB:
type: object
description: ' Child B'
allOf:
- $ref: '#/components/schemas/ParentClass'
ContainerClass:
type: object
properties:
elements:
type: array
description: array schema description
items:
oneOf:
- $ref: '#/components/schemas/ChildA'
- $ref: '#/components/schemas/ChildB'
ParentClass:
type: object
properties:
classType:
type: string
description: Parent description
discriminator:
propertyName: classType
mapping:
CHILD_A: '#/components/schemas/ChildA'
CHILD_B: '#/components/schemas/ChildB'
Actually, in my context, in order to have not any breaking change with existing consumers, I need items property in ContainerClass schema to contain the discriminator part that is contained in ParentClass schema, like this:
ContainerClass:
type: object
properties:
elements:
type: array
description: array schema description
items:
discriminator:
propertyName: classType
mapping:
CHILD_A: '#/components/schemas/ChildA'
CHILD_B: '#/components/schemas/ChildB'
oneOf:
- $ref: '#/components/schemas/ChildA'
- $ref: '#/components/schemas/ChildB'
When I try to set properties in annotation, I don't manage to do that. And when I debug code of io.swagger.v3.core.jackson.ModelResolver, I don't manage to find a way to do that.
And so far I have not found an example of code that help me.
Is there a way so that a ComposedSchema (array contained in ContainerClass in my case) has a disciminator part generated by springdoc-openapi-maven-plugin execution?

This the default generation structure. Handled directly by swagger-api (and not springdoc-openapi.
The generated OpenAPI description looks coorect.
With springdoc-openapi, you can define an OpenApiCustomiser Bean, where you can change the elements of the components element defined on the OpenAPI level:
https://springdoc.org/faq.html#how-can-i-customise-the-openapi-object-

Here is my solution by defining an OpenApiCustomiser Bean:
#Bean
public OpenApiCustomiser myCustomiser() {
Map<String, String> classTypeMapping = Map.ofEntries(
new AbstractMap.SimpleEntry<String, String>("CHILD_A", "#/components/schemas/ChildA"),
new AbstractMap.SimpleEntry<String, String>("CHILD_B", "#/components/schemas/ChildB")
);
Discriminator classTypeDiscriminator = new Discriminator().propertyName("classType")
.mapping(classTypeMapping);
return openApi -> openApi.getComponents().getSchemas().values()
.stream()
.filter(schema -> "ContainerClass".equals(schema.getName()))
.map(schema -> schema.getProperties().get("elements"))
.forEach(arraySchema -> ((ArraySchema)arraySchema).getItems().discriminator(classTypeDiscriminator));
}
I get the expected result in my contract file.

Related

Hibernate diffChangeLog producing change sets that exist already in the table

I am using spring boot with hibernate 5.3.8. After I run the first schema I generate a diffChangeLog and its dropping and creating the same index. When I look at the collection_days table a common_collection_search exists
How can I mark it as completed so it no longer appears in the diffChangeLog. I only want generate a changelog with the newest changes so I can run it and update the database
collection_days table
index_name|index_algorithm|is_unique|column_name|condition
common_collection_search|BTREE|f|waste_generator_id,refuse_type|NULL
This is from my master schema that I ran on a fresh database
- changeSet:
id: 1572649828026-46
author: comp (generated)
changes:
- createIndex:
columns:
- column:
name: wg_id
- column:
name: type
indexName: common_collection_search
tableName: collection_days
diffChangeLog results
- changeSet:
id: 1615578168770-16
author: comp (generated)
changes:
- dropIndex:
indexName: common_collection_search
tableName: collection_days
- changeSet:
id: 1615578168770-17
author: comp (generated)
changes:
- createIndex:
columns:
- column:
name: wg_id
- column:
name: type
indexName: common_collection_search
tableName: collection_days
CollectionDay entity
#Data
#Entity
#Table(name = "collection_days", uniqueConstraints = {
#UniqueConstraint(columnNames = { "wg_id", "type", "day_of_week" }) }, indexes = {
#Index(name = "common_collection_search", columnList = "wg_id,type") })
public class CollectionDay implements Serializable {
#Column(name = "wg_id",updatable = false,insertable = false)
private Long wgId;
#Column(name = "type")
#Enumerated(EnumType.STRING)
private ESEnums.type type;
build.gradle
compile "org.hibernate:hibernate-core"
compile "org.hibernate:hibernate-entitymanager"
compile "org.hibernate:hibernate-envers"
compile "org.springframework.boot:spring-boot-starter-data-jpa"
compile "com.zaxxer:HikariCP"
compile "org.hibernate:hibernate-ehcache"
compile 'com.fasterxml.jackson.datatype:jackson-datatype-hibernate5'
compile "org.postgresql:postgresql"
compile "org.liquibase:liquibase-core"
compile "org.hibernate:hibernate-validator:6.1.6.Final"
liquibaseRuntime 'org.liquibase.ext:liquibase-hibernate5:3.8'
liquibaseRuntime sourceSets.main.output
liquibaseRuntime sourceSets.main.compileClasspath
liquibase {
activities {
main {
driver 'org.postgresql.Driver'
url 'jdbc:postgresql://localhost:54323/db2?stringtype=unspecified'
username ''
password ''
changeLogFile "src/main/resources/liquibase/migrations/changelog.yaml"
classpath 'src/main/java'
referenceUrl 'hibernate:spring:com.project.domain.entities?dialect=org.hibernate.dialect.PostgreSQL82Dialect&hibernate.physical_naming_strategy=org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy&hibernate.implicit_naming_strategy=org.springframework.boot.orm.jpa.hibernate.SpringImplicitNamingStrategy'
}
}
}
I think the problem is that PostgreSQL is smart enough to realize that the index is unnecessary and doesn't create it in the way you expect it. Your unique constraint already covers the columns of the index and since it is backed by an index, it will just tell you that it won't create your index, because it is already covered.

Unable to generate example json value from yaml

I am trying to parse a given yaml to a JSON example like swagger does. I am using the below yaml.
$id: 'https://example.com/person.schema.json'
$schema: 'http://json-schema.org/draft-07/schema#'
title: Person
type: object
properties:
firstName:
type: string
example:MyName
description: The person's first name.
lastName:
type: string
description: The person's last name.
age:
description: Age in years which must be equal to or greater than zero.
type: integer
minimum: 0
I am trying to parse this yaml to its json like the below one. If the example is given in the yaml then it should show the example else it should show a default value.
Person:{
"firstName":"MyName",
"lastName":"string",
"age":0
}
But while parsing I am getting an Exception:
com.fasterxml.jackson.core.JsonParseException: Unrecognized token '$id': was expecting ('true', 'false' or 'null')
at [Source: (String)"$id: 'https://example.com/person.schema.json'
line: 1, column: 4]
at com.fasterxml.jackson.core.JsonParser._constructError(JsonParser.java:1804) ~[jackson-core-2.9.8.jar:2.9.8]
at com.fasterxml.jackson.core.base.ParserMinimalBase._reportError(ParserMinimalBase.java:703) ~[jackson-core-2.9.8.jar:2.9.8]
at com.fasterxml.jackson.core.json.ReaderBasedJsonParser._reportInvalidToken(ReaderBasedJsonParser.java:2853) ~[jackson-core-2.9.8.jar:2.9.8]
at com.fasterxml.jackson.core.json.ReaderBasedJsonParser._handleOddValue(ReaderBasedJsonParser.java:1899) ~[jackson-core-2.9.8.jar:2.9.8]
at com.fasterxml.jackson.core.json.ReaderBasedJsonParser.nextToken(ReaderBasedJsonParser.java:757) ~[jackson-core-2.9.8.jar:2.9.8]
at com.fasterxml.jackson.databind.ObjectMapper._readTreeAndClose(ObjectMapper.java:4042) ~[jackson-databind-2.9.8.jar:2.9.8]
at com.fasterxml.jackson.databind.ObjectMapper.readTree(ObjectMapper.java:2551) ~[jackson-databind-2.9.8.jar:2.9.8]
at io.swagger.parser.SwaggerCompatConverter.readResourceListing(SwaggerCompatConverter.java:210) [swagger-compat-spec-parser-1.0.44.jar:1.0.44]
at io.swagger.parser.SwaggerCompatConverter.read(SwaggerCompatConverter.java:123) [swagger-compat-spec-parser-1.0.44.jar:1.0.44]
at io.swagger.parser.SwaggerCompatConverter.readWithInfo(SwaggerCompatConverter.java:94) [swagger-compat-spec-parser-1.0.44.jar:1.0.44]
at io.swagger.parser.SwaggerParser.readWithInfo(SwaggerParser.java:42) [swagger-parser-1.0.44.jar:1.0.44]
at io.swagger.v3.parser.converter.SwaggerConverter.readLocation(SwaggerConverter.java:92) [swagger-parser-v2-converter-2.0.12.jar:2.0.12]
at io.swagger.v3.parser.OpenAPIV3Parser.read(OpenAPIV3Parser.java:90) [swagger-parser-v3-2.0.12.jar:2.0.12]
at io.swagger.v3.parser.OpenAPIV3Parser.read(OpenAPIV3Parser.java:78) [swagger-parser-v3-2.0.12.jar:2.0.12]
at com.pega.yaml.yamlToJsonExample.App.main(App.java:19) [classes/:na]
Exception in thread "main" java.lang.NullPointerException
at io.swagger.v3.parser.OpenAPIV3Parser.read(OpenAPIV3Parser.java:91)
at io.swagger.v3.parser.OpenAPIV3Parser.read(OpenAPIV3Parser.java:78)
at com.pega.yaml.yamlToJsonExample.App.main(App.java:19)
I am using swagger inflector 2.0.5 with the following code:
import io.swagger.v3.parser.OpenAPIV3Parser;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.media.Schema;
import io.swagger.oas.inflector.examples.models.Example;
import io.swagger.oas.inflector.examples.ExampleBuilder;
import java.util.Map;
import com.fasterxml.jackson.databind.module.SimpleModule;
import io.swagger.oas.inflector.processors.JsonNodeExampleSerializer;
import io.swagger.util.Json;
public class App
{
public static void main( String[] args )
{
OpenAPI swagger = new OpenAPIV3Parser().read("Eclipse Workspace1\\yamlToJsonExample\\src\\main\\java\\com\\pega\\yaml\\yamlToJsonExample\\Test.yaml");
Map < String, Schema > definitions = swagger.getComponents().getSchemas();
Schema model = definitions.get("Person");
Example example = ExampleBuilder.fromSchema(model, definitions);
SimpleModule simpleModule = new SimpleModule().addSerializer(new JsonNodeExampleSerializer());
Json.mapper().registerModule(simpleModule);
String jsonExample = Json.pretty(example);
System.out.println(jsonExample);
}
}

How to Define Enum as Query Params in OpenAPI Spec

I want to define an enum value in the query string parameter of a function in an OpenAPI specification.
Here is an example of how I specify the parameter in my OpenAPI specification (yaml):
openapi: 3.0.0
info:
description: Vends sodas
version: 1.0.0
title: VendingMachineService
paths:
/soda/{bill}:
get:
summary: Provides a soda for a provided amount of money
description: Provides a soda for a provided amount of money
operationId: getSoda
parameters:
- name: bill
in: path
description: Money (e.g. one, two, five)
required: true
schema:
$ref: "#/components/schemas/Bill"
responses:
"200":
description: Success
content:
application/json:
schema:
$ref: "#/components/schemas/Result"
"404":
description: Templates for Source not found
servers:
- url: http://localhost:8080/
components:
schemas:
Bill:
type: string
enum:
- one
- two
- five
Result:
type: string
The key part of the OpenAPI spec is that I define the enum in a schema object at the bottom, type string, and refer to it in the parameters section of the function, also defining it as part of the path /{bill}
I then use openapi-generator to generate the spring server:
openapi-generator generate -i ~/vending-machine.yaml -g spring -o ~/output
I then spin up the server (importing the project into Intellij and running in a spring-boot configuration) and go to localhost:8080/ to open the Swagger UI. I try to exercise the api and get the following error:
{ "timestamp": "2019-03-29T15:43:14.737Z", "status": 400,
"error": "Bad Request", "message": "Failed to convert value of type
'java.lang.String' to required type 'org.openapitools.model.Bill';
nested exception is
org.springframework.core.convert.ConversionFailedException: Failed to
convert from type [java.lang.String] to type
[#javax.validation.constraints.NotNull
#io.swagger.annotations.ApiParam #javax.validation.Valid
#org.springframework.web.bind.annotation.RequestParam
org.openapitools.model.Bill] for value 'null'; nested exception is
java.lang.IllegalArgumentException: No enum constant
org.openapitools.model.Bill.null", "path": "/soda/%7Bbill%7D" }
(501 Not Implemented expected)
What is the proper way to define the query params & enum in the OpenAPI spec to use enum values?

Retrieve #Authorization swagger codegen java

I work with swagger 2.0 to define a back end and trying to define security.
I end up with :
---
swagger: "2.0"
info:
version: 1.0.0
title: Foo test
schemes:
- https
paths:
/foo:
get:
security:
- Foo: []
responses:
200:
description: Ok
securityDefinitions:
Foo:
type: apiKey
name: X-BAR
in: header
Everything good till now, java codegen give me :
#ApiOperation(value = "", nickname = "fooGet", notes = "", authorizations = {
#Authorization(value = "Foo")
}, tags={ })
#ApiResponses(value = {
#ApiResponse(code = 200, message = "Ok") })
#RequestMapping(value = "/foo",
method = RequestMethod.GET)
default ResponseEntity<Void> fooGet() {
if(getObjectMapper().isPresent() && getAcceptHeader().isPresent()) {
} else {
log.warn("ObjectMapper or HttpServletRequest not configured in default FooApi interface so no example is generated");
}
return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED);
}
I'm wondering, in the interface, how to retrieve "properly" the X-BAR header value.
I end up with :
#Autowired
private HttpServletRequest httpServletRequest;
httpServletRequest.getHeader("X-BAR")
which works , but is there a more proper way ?
Define a class "Foo" ? a doFilter ?
Thanks

Internationalization for user defined input

I'm trying to build a localization for user defined input. Example: a user could define categories like soccer but in several languages.
The model entity could have a filed something like this:
#ManyToMany
#MapKeyColumn(name = "locale", insertable = false, updatable = false)
public Map<String, L18n> titles;
I'd like to store the localized strings like this:
#Entity
public class L18n {
#Id
#Constraints.Required
#Formats.NonEmpty
public Integer id;
public String key;
public String locale;
#Column(columnDefinition = "TEXT")
public String text;
}
We use yaml to store the testdata:
category:
- !!models.Category
...
titles:
- !!models.L18n
key: soccer
l18n:
- !!models.L18n
key: soccer
locale: de-CH
text: fdfdfsee
- !!models.L18n
key: soccer
locale: fr-CH
text: dlfkjsdlfj
With this solution I'm getting this error:
[error] Caused by: org.yaml.snakeyaml.error.YAMLException: No suitable constructor with 1 arguments found for interface java.util.Map
[error] at org.yaml.snakeyaml.constructor.Constructor$ConstructSequence.construct(Constructor.java:574)
[error] at org.yaml.snakeyaml.constructor.BaseConstructor.constructObject(BaseConstructor.java:182)
[error] at org.yaml.snakeyaml.constructor.Constructor$ConstructMapping.constructJavaBean2ndStep(Constructor.java:296)
[error] ... 65 more
I think the problem is you've defined titles as a map, but don't use the YAML mapping syntax to define it. YAML Collections
I think the syntax for titles should look like the example here using langauges: YAML Dictionary Example
titles:
soccer: - !!models.L18n
key: soccer
locale: de-CH
text: fdfdfsee
football: - !!models.L18n
...
If you change the format of your .yml file so that the L18n instances are defined first and use references, the YAML spec calls them Alias Nodes, you could make your titles look a lot more streamlined:
l18n:
- &soccerDE !!models.L18n
key: soccer
locale: de-CH
text: fdfdfsee
- &soccerFR !!models.L18n
key: soccer
locale: fr-CH
text: dlfkjsdlfj
category:
- !!models.Category
...
titles:
soccerDE: *soccerDE
soccerFR: *soccerFR

Resources