I am in the process of taking a look into polymorphism for OpenAPI. More specifically I am in trying out the example with the Pet, Cat and Dog found here using this portion of yaml:
components:
schemas:
Pet:
type: object
discriminator:
propertyName: petType
properties:
name:
type: string
petType:
type: string
required:
- name
- petType
Cat: ## "Cat" will be used as the discriminator value
description: A representation of a cat
allOf:
- $ref: '#/components/schemas/Pet'
- type: object
properties:
huntingSkill:
type: string
description: The measured skill for hunting
enum:
- clueless
- lazy
- adventurous
- aggressive
required:
- huntingSkill
Dog: ## "Dog" will be used as the discriminator value
description: A representation of a dog
allOf:
- $ref: '#/components/schemas/Pet'
- type: object
properties:
packSize:
type: integer
format: int32
description: the size of the pack the dog is from
default: 0
minimum: 0
required:
- packSize
Using this I am able to create the DTOs using the OpenAPI spring generator correctly and with a proper structure. Now all seem fine up to this step but when I serve the SwaggerUI using the Spring's dependencies the generated subclasses are not served as part of the schema. For instance, when I fetch the JSON definition of this I get the following:
{
"openapi": "3.0.1",
"info": {
"title": "OpenAPI definition",
"version": "v0"
},
"servers": [{
"url": "http://localhost:8080",
"description": "Generated server url"
}],
"tags": [{
"name": "Test",
"description": "the Test API"
}],
"paths": {
"/test": {
"get": {
"tags": ["Test"],
"operationId": "testGet",
"responses": {
"200": {
"description": "A list of search results",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Pet"
}
}
}
}
}
}
}
},
"components": {
"schemas": {
"Pet": {
"required": ["name", "petType"],
"type": "object",
"properties": {
"name": {
"type": "string"
},
"petType": {
"type": "string"
}
},
"discriminator": {
"propertyName": "petType"
}
}
}
}
}
As we can see there is no Cat or Dog definition. This in turn makes consumers of this JSON definition to incorrectly generate their code.
My question is thus, what needs to be done in order to get this working properly. For reference I am using OpenAPI 3.0.1, SpringBoot 3.0 and the following swagger and springdoc dependencies:
implementation 'org.openapitools:jackson-databind-nullable:0.2.4'
implementation 'io.swagger.core.v3:swagger-annotations:2.2.7'
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.0.0'
Related
I'm writing a schema using draft/2020-12 to validate an array of multiple objects. I will be using YAML for the examples for readability (and it's YAML documents that are going to be validated).
I've seen many different approaches but none seem to do a very good job for some reason.
The data to validate
pipeline:
- foo: abc
someOption: 123
- baz: def
anotherOption: 223
- foo: ghi
As you can see, the pipeline array contains objects that has completely different properties. The same object types can appear multiple times (there's 2 instances of an object containing foo, they are to be seen as the same object type).
Schema
$id: 'https://example.com/schema'
$schema: 'https://json-schema.org/draft/2020-12/schema'
type: object
properties:
pipeline:
type: array
additionalItems: false
anyOf:
- items:
type: object
additionalProperties: false
properties:
foo:
type: string
someOption:
type: integer
required: [foo]
- items:
type: object
additionalProperties: false
properties:
baz:
type: string
anotherOption:
type: integer
required: [baz]
To me, this looks correct, although it fails validation and I can't quite see why...
Even if it had worked, I'm not sure if I'm supposed to use anyOf or oneOf in this case. Is it matching across all array items or per individual array item?
You have it slightly wrong. Currently your schema says... for the pipeline array, either, all of the items must be foo or all of the items must be baz.
You need to change your location of anyOf and items...
items applies to every item in the array.
anyOf checks that any of the subschema values are valid against the instance location (in this case, each item in the array).
{
"$id": "https://example.com/schema",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"pipeline": {
"type": "array",
"items": {
"anyOf": [
{
"type": "object",
"additionalProperties": false,
"properties": {
"foo": {
"type": "string"
},
"someOption": {
"type": "integer"
}
},
"required": [
"foo"
]
}, {
"type": "object",
"additionalProperties": false,
"properties": {
"baz": {
"type": "string"
},
"anotherOption": {
"type": "integer"
}
},
"required": [
"baz"
]
}
]
}
}
}
}
I'm using go-restful in combination with go-restful-openapi to generate my swagger doc automatically. However, on testing the tool with the swagger-editor, I get the following error:
$refs must reference a valid location in the document
struct
type Users struct {
# uuid imported from github.com/google/uuid#v1.2.0
RelatedUsers []uuid.UUID `json:"relatedIds" validate:"required"`
}
generated swagger snippet
"Users": {
"required": [
"relatedIds"
],
"properties": {
"relatedIds": {
"type": "array",
"$ref": "#/definitions/uuid.UUID" #this line returns an error
}
}
}
}
Here's the swagger config:
swaggerConfig := restfulspec.Config{
WebServices: restfulContainer.RegisteredWebServices(),
APIPath: "/swagger.json",
PostBuildSwaggerObjectHandler: func(swo *spec.Swagger) {
swo.Info = &spec.Info{
InfoProps: spec.InfoProps{
Title: "User Service",
Description: "An example service for stackoverflow",
Version: "1.0.0",
},
}
},
}
NOTE: If I replace the lines above in the swagger editor as follows the error disappears, however, I couldn't figure out how to configure swagger to do so automatically
"Users": {
"required": [
"relatedIds"
],
"properties": {
"relatedUsers": {
type: array
items:
type: "string"
format: "uuid"
}
}
}
}
Just in case someone is running into this issue: go-restful-openapi cannot resolve imported custom types. To solve this issue add the type to the definitions
swo.Definitions["uuid.UUID"] = spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"string"},
Format: "uuid",
},
}
I tried to make schema to validate json such this:
{
"integration": { "module": [ "m" ] },
"tile": {
"title": "TTT",
"text": "ttt",
"icon": "./resources/main-icon.png",
"tags": [ "bbb", "vvvv"],
"orderNumber": 20
},
"steps": {
"order": [
"1",
"2",
"3"
],
"data": {
"1": {
"title": "tt1",
"description": "",
"screens": { "default": "true" }
},
"2": {
"title": "tt2",
"description": "",
"screens": { "default": "true" }
},
"3": {
"title": "tt3",
"description": "",
"screens": { "default": "true" }
}
}
}
};
Schema:
Joi.object({
integration: Joi.object({
module: Joi.array().items(Joi.string().valid('m').required())
}).required(),
tile: Joi.object({
title: Joi.string().required(),
text: Joi.string().required(),
icon: Joi.string().required(),
tags: Joi.array().items(Joi.string()).required(),
orderNumber: Joi.number().integer().min(1).max(255).required()
}).required(),
steps: Joi.object({
order: Joi.array().items(Joi.string()).required(),
data: Joi.object().keys({
title: Joi.string().required(),
description: Joi.string().required(),
screens: Joi.object({
default: Joi.string().valid('true', 'false').required()
}).required()
}).unknown(),
}).required()
});
But it generate error:
Validation Error: "steps.data.title" is required. "steps.data.description" is required. "steps.data.screens" is required
Please help. How can I make this schema?
Your data key is an object with keys 1, 2, and 3, each one is also an object with keys title, description, and screens.
But in your validation, your data key is an object with keys title, description, and screens, which is not correct.
You should change your steps.data validation to this:
data: Joi.object().pattern(
Joi.string().valid("1", "2", "3"),
Joi.object().keys({
title: Joi.string().required(),
description: Joi.string().required().allow(''),
screens: Joi.object({ default: Joi.string().valid('true', 'false').required() }),
})).unknown(),
}).required()
I used Joi.object().pattern to avoid duplicating the code since your object value is the same for each key.
I also changed your data.description, since you were not allowing empty strings, I just added .allow('').
I'm using the Go implemenatation of GraphQL.
How would you configure a mutation so that it can receive arguments with more than 1 level?
For exemple, here is the list of arguments I would like to pass to a mutation CreateUser:
mutation createUser($user: CreateUser!) {
createUser(input: $user)
}
{
"user": {
"name": {
"first": "John",
"last": "Doe"
},
"email": "john#doe.com"
}
}
(Notice that I dont want to use firstname and lastname but a name object instead)
And this is my (unsuccessful) attempt so far:
var CreateUserInput = graphql.FieldConfigArgument{
"input": &graphql.ArgumentConfig{
Description: "Input for creating a new user",
Type: graphql.NewNonNull(graphql.NewInputObject(graphql.InputObjectConfig{
Name: "CreateUser",
Fields: graphql.InputObjectConfigFieldMap{
"name": &graphql.InputObjectFieldConfig{
Type: graphql.NewNonNull(graphql.NewInputObject(graphql.InputObjectConfig{
Fields: graphql.InputObjectConfigFieldMap{
"first": &graphql.InputObjectFieldConfig{
Type: graphql.NewNonNull(graphql.String),
},
"last": &graphql.InputObjectFieldConfig{
Type: graphql.NewNonNull(graphql.String),
},
},
})),
},
"email": &graphql.InputObjectFieldConfig{
Type: graphql.NewNonNull(graphql.String),
},
},
})),
},
}
Apparently the subfields first and last are not recognized as this is what I get when I run this mutation:
{
"data": null,
"errors": [
{
"message": "Variable \"$user\" got invalid value {\"email\":\"john#doe.com\",\"name\":{\"first\":\"john\",\"last\":\"doe\"}}.\nIn field \"name\": In field \"first\": Unknown field.\nIn field \"name\": In field \"last\": Unknown field.",
"locations": [
{
"line": 1,
"column": 21
}
]
}
]
}
Is this even possible?
EDIT: See comments in the accepted answer for the solution.
This are my first ever lines of Go but I will try to convey what I think the problem is.
First lets talk about the structure you want to be going for. I will use SDL here:
type Mutation {
createUser(user: CreateUser!): Boolean! # Maybe return user type here?
}
input CreateUser {
name: CreateUserName!
email: String!
}
input CreateUserName {
first: String!
last: String!
}
Okay now that we know that we need two input types lets get started!
var CreateUserName = graphql.NewInputObject(graphql.InputObjectConfig{
Name: "CreateUserName",
Fields: graphql.InputObjectConfigFieldMap{
"first": &graphql.InputObjectFieldConfig{
Type: graphql.NewNonNull(graphql.String),
},
"last": &graphql.InputObjectFieldConfig{
Type: graphql.NewNonNull(graphql.String),
},
},
})
var CreateUser = graphql.NewInputObject(graphql.InputObjectConfig{
Name: "CreateUser",
Fields: graphql.InputObjectConfigFieldMap{
"name": &graphql.InputObjectFieldConfig{
Type: graphql.NewNonNull(CreateUserName),
},
"email": &graphql.InputObjectFieldConfig{
Type: graphql.NewNonNull(graphql.String),
},
},
})
Now all that should be left is adding the mutation field to your mutation object type.
I am using graphQL to perform a search across multiple mongoDB collections and API's by combining queries. All queries return a result type of
{
_id: string;
name: string;
type: string;
}
Is there any way to flatten the data into a single array?
Combined query example:
query searchAll {
books(input: {text: "e"}) {
_id
name
type
}
magazines(input: {text: "e"}) {
_id
name
type
}
}
Response currently looks like:
{"data": {
"books": [
{
"_id": "5a8ac759c25b7235ffdc6888",
"name": "someBook",
"type": "book"
}
],
"magazines": [
{
"_id": "5a87005bc25b7235ffdc4bdf",
"name": "someMagazine-1",
"type": "magazine"
},
{
"_id": "5a870067c25b7235ffdc4be4",
"name": "someMagazine-2",
"type": "client"
}
]
}
}
Desired response:
{"data": {
"results": [
{
"_id": "5a8ac759c25b7235ffdc6888",
"name": "someBook",
"type": "book"
},
{
"_id": "5a87005bc25b7235ffdc4bdf",
"name": "someMagazine-1",
"type": "magazine"
},
{
"_id": "5a870067c25b7235ffdc4be4",
"name": "someMagazine-2",
"type": "client"
}
]
}
}
You want to look into using interfaces, here's an example of a (slightly richer) schema definition:
interface Searchable {
id: ID!
name: String!
}
type Book implements Searchable {
id: ID!
name: String!
author: Author!
publisher: Publisher!
isbn: String!
}
type Magazine implements Searchable {
id: ID!
name: String!
publisher: Publisher!
issn: String!
}
input SearchInput {
text: String!
}
type Query {
search(input: SearchInput!): [Searchable!]!
}
Here's how you'd query it:
query searchAll {
search(input: {text: "e"}) {
__typename
id
name
... on Book {
author
isbn
}
... on Magazine {
issn
}
}
}
The resolver for search would be responsible for calling all the different collections and aggregating the results into a single array. Going into more depth than this is implementation-specific, but there should be docs for using interfaces (and unions, which are similar) in whichever GraphQL implementation you're using.
The __typename field is what tells you the concrete type of the returned object, thereby letting front-end code do the appropriate display logic.