diff --git a/packages/openapi-to-graphql/src/preprocessor.ts b/packages/openapi-to-graphql/src/preprocessor.ts index c3356820..70cfb0c3 100644 --- a/packages/openapi-to-graphql/src/preprocessor.ts +++ b/packages/openapi-to-graphql/src/preprocessor.ts @@ -27,6 +27,8 @@ import debug from 'debug' import { handleWarning, getCommonPropertyNames, MitigationTypes } from './utils' import { GraphQLOperationType } from './types/graphql' import { methodToHttpMethod } from './oas_3_tools' +import { GraphQLObjectType } from 'graphql' +import { getGraphQLType } from './schema_builder' const preprocessingLog = debug('preprocessing') @@ -651,7 +653,8 @@ export function createDataDef( isInputObjectType: boolean, data: PreprocessingData, oas: Oas3, - links?: { [key: string]: LinkObject } + links?: { [key: string]: LinkObject }, + resolveDiscriminator: boolean = true ): DataDefinition { const preferredName = getPreferredName(names) @@ -866,6 +869,26 @@ export function createDataDef( } } + /** + * Union types will be extracted either from the discriminator mapping + * or from the enum list defined for discriminator property + */ + if (hasDiscriminator(schema) && resolveDiscriminator) { + const unionDef = createDataDefFromDiscriminator( + saneName, + schema, + isInputObjectType, + def, + data, + oas + ) + + if (unionDef && typeof unionDef === 'object') { + def.targetGraphQLType = 'json' + return def + } + } + if (targetGraphQLType) { switch (targetGraphQLType) { case 'list': @@ -944,6 +967,12 @@ export function createDataDef( } } +// Checks if schema object has discriminator field +function hasDiscriminator(schema: SchemaObject): boolean { + const collapsedSchema: SchemaObject = JSON.parse(JSON.stringify(schema)) + return collapsedSchema.discriminator?.propertyName ? true : false +} + /** * Returns the index of the data definition object in the given list that * contains the same schema and preferred name as the given one. Returns -1 if @@ -1138,6 +1167,248 @@ function addObjectPropertiesToDataDef( } } +/** + * Iterate through discriminator object mapping or through discriminator + * enum values and resolve derived schemas + */ +function createDataDefFromDiscriminator( + saneName: string, + schema: SchemaObject, + isInputObjectType = false, + def: DataDefinition, + data: PreprocessingData, + oas: Oas3 +): DataDefinition { + /** + * Check if discriminator exists and if it has + * defined property name + */ + if (!schema.discriminator?.propertyName) { + return null + } + + const unionTypes: DataDefinition[] = [] + const schemaToTypeMap: Map = new Map() + + // Get the discriminator property name + const discriminator = schema.discriminator.propertyName + + /** + * Check if there is defined property pointed by discriminator + * and if that property is in the required properties list + */ + if ( + schema.properties && + schema.properties[discriminator] && + schema.required && + schema.required.indexOf(discriminator) > -1 + ) { + let discriminatorProperty = schema.properties[discriminator] + + // Dereference discriminator property + if ('$ref' in discriminatorProperty) { + discriminatorProperty = Oas3Tools.resolveRef( + discriminatorProperty['$ref'], + oas + ) as SchemaObject + } + + /** + * Check if there is mapping defined for discriminator property + * and iterate through the map in order to generate derived types + */ + if (schema.discriminator.mapping) { + for (const key in schema.discriminator.mapping) { + const unionTypeDef = createUnionSubDefinitionFromDiscriminator( + schema, + saneName, + schema.discriminator.mapping[key], + isInputObjectType, + data, + oas + ) + + if (unionTypeDef) { + unionTypes.push(unionTypeDef) + schemaToTypeMap.set(key, unionTypeDef.preferredName) + } + } + } else if ( + /** + * If there is no defined mapping, check if discriminator property + * schema has defined enum, and if enum exists iterate through + * the enum values and generate derived types + */ + discriminatorProperty.enum && + discriminatorProperty.enum.length > 0 + ) { + const discriminatorAllowedValues = discriminatorProperty.enum + discriminatorAllowedValues.forEach((enumValue) => { + const unionTypeDef = createUnionSubDefinitionFromDiscriminator( + schema, + saneName, + enumValue, + isInputObjectType, + data, + oas + ) + + if (unionTypeDef) { + unionTypes.push(unionTypeDef) + schemaToTypeMap.set(enumValue, unionTypeDef.preferredName) + } + }) + } + } + + // Union type will be created if unionTypes list is not empty + if (unionTypes.length > 0) { + const iteration = 0 + + /** + * Get GraphQL types for union type members so that + * these types can be used in resolveType method for + * this union + */ + const types = Object.values(unionTypes).map((memberTypeDefinition) => { + return getGraphQLType({ + def: memberTypeDefinition, + data, + iteration: iteration + 1, + isInputObjectType + }) as GraphQLObjectType + }) + + /** + * TODO: Refactor this when GraphQL receives a support for input unions. + * Create DataDefinition object for union with custom resolveType function + * which resolves union types based on discriminator provided in the Open API + * schema. The union data definition should be used for generating response + * type and for inputs parent data definition should be used + */ + def.unionDefinition = { + ...def, + targetGraphQLType: 'union', + subDefinitions: unionTypes, + resolveType: (source, context, info) => { + // Find the appropriate union member type + return types.find((type) => { + // Check if source contains not empty discriminator field + if (source[discriminator]) { + const typeName = schemaToTypeMap.get(source[discriminator]) + return typeName === type.name + } + + return false + }) + } + } + + return def + } + + return null +} + +function createUnionSubDefinitionFromDiscriminator( + unionSchema: SchemaObject, + unionSaneName: string, + subSchemaName: string, + isInputObjectType: boolean, + data: PreprocessingData, + oas: Oas3 +): DataDefinition { + // Find schema for derived type using schemaName + let schema = oas.components.schemas[subSchemaName] + + // Resolve reference + if (schema && '$ref' in schema) { + schema = Oas3Tools.resolveRef(schema['$ref'], oas) as SchemaObject + } + + if (!schema) { + handleWarning({ + mitigationType: MitigationTypes.MISSING_SCHEMA, + message: `Resolving schema from discriminator with name ${subSchemaName} in schema '${JSON.stringify( + unionSchema + )} failed because such schema was not found.`, + data, + log: preprocessingLog + }) + return null + } + + if (!isSchemaDerivedFrom(schema, unionSchema, oas)) { + return null + } + + const collapsedSchema = resolveAllOf(schema, {}, data, oas) + + if ( + collapsedSchema && + Oas3Tools.getSchemaTargetGraphQLType(collapsedSchema, data) === 'object' + ) { + let subNames = {} + if (deepEqual(unionSchema, schema)) { + subNames = { + fromRef: `${unionSaneName}Member`, + fromSchema: collapsedSchema.title + } + } else { + subNames = { + fromRef: subSchemaName, + fromSchema: collapsedSchema.title + } + } + + return createDataDef( + subNames, + schema, + isInputObjectType, + data, + oas, + {}, + false + ) + } + + return null +} + +/** + * Check if child schema is derived from parent schema by recursively + * looking into schemas references in child's allOf property + */ +function isSchemaDerivedFrom( + childSchema: SchemaObject, + parentSchema: SchemaObject, + oas: Oas3 +) { + if (!childSchema.allOf) { + return false + } + + for (const allOfSchema of childSchema.allOf) { + let resolvedSchema: SchemaObject = null + if (allOfSchema && '$ref' in allOfSchema) { + resolvedSchema = Oas3Tools.resolveRef( + allOfSchema['$ref'], + oas + ) as SchemaObject + } else { + resolvedSchema = allOfSchema + } + + if (deepEqual(resolvedSchema, parentSchema)) { + return true + } else if (isSchemaDerivedFrom(resolvedSchema, parentSchema, oas)) { + return true + } + } + + return false +} + /** * Recursively traverse a schema and resolve allOf by appending the data to the * parent schema @@ -1284,23 +1555,26 @@ function getMemberSchemaData( schema = Oas3Tools.resolveRef(schema['$ref'], oas) as SchemaObject } + const collapsedSchema = resolveAllOf(schema, {}, data, oas) + // Consolidate target GraphQL type const memberTargetGraphQLType = Oas3Tools.getSchemaTargetGraphQLType( - schema, + collapsedSchema, data ) + if (memberTargetGraphQLType) { result.allTargetGraphQLTypes.push(memberTargetGraphQLType) } // Consolidate properties - if (schema.properties) { - result.allProperties.push(schema.properties) + if (collapsedSchema.properties) { + result.allProperties.push(collapsedSchema.properties) } // Consolidate required - if (schema.required) { - result.allRequired = result.allRequired.concat(schema.required) + if (collapsedSchema.required) { + result.allRequired = result.allRequired.concat(collapsedSchema.required) } }) @@ -1587,21 +1861,32 @@ function createDataDefFromOneOf( ) as SchemaObject } + const collapsedMemberSchema = resolveAllOf( + memberSchema, + {}, + data, + oas + ) + // Member types of GraphQL unions must be object types if ( - Oas3Tools.getSchemaTargetGraphQLType(memberSchema, data) === - 'object' + Oas3Tools.getSchemaTargetGraphQLType( + collapsedMemberSchema, + data + ) === 'object' ) { const subDefinition = createDataDef( { fromRef, - fromSchema: memberSchema.title, + fromSchema: collapsedMemberSchema.title, fromPath: `${saneName}Member` }, memberSchema, isInputObjectType, data, - oas + oas, + {}, + false ) ;(def.subDefinitions as DataDefinition[]).push(subDefinition) } else { diff --git a/packages/openapi-to-graphql/src/schema_builder.ts b/packages/openapi-to-graphql/src/schema_builder.ts index 89f7f601..461b3546 100644 --- a/packages/openapi-to-graphql/src/schema_builder.ts +++ b/packages/openapi-to-graphql/src/schema_builder.ts @@ -139,6 +139,16 @@ export function getGraphQLType({ }: CreateOrReuseComplexTypeParams): | GraphQLOutputType | GraphQLInputType { + /** + * TODO: Refactor this when GraphQL receives a support for input unions. + * If this metod is not processing input object type and passed data definition + * contains nested union definition, the passed data definition should be replaced + * by the nested union definition + */ + if (def.unionDefinition && !isInputObjectType) { + def = def.unionDefinition + } + const name = isInputObjectType ? def.graphQLInputObjectTypeName : def.graphQLTypeName @@ -366,33 +376,37 @@ function createOrReuseUnion({ name: def.graphQLTypeName, description, types, - resolveType: (source, context, info) => { - const properties = Object.keys(source) - // Remove custom _openAPIToGraphQL property used to pass data - .filter((property) => property !== '_openAPIToGraphQL') - - /** - * Find appropriate member type - * - * TODO: currently, the check is performed by only checking the property - * names. In the future, we should also check the types of those - * properties. - * - * TODO: there is a chance a that an intended member type cannot be - * identified if, for whatever reason, the return data is a superset - * of the fields specified in the OAS - */ - return types.find((type) => { - const typeFields = Object.keys(type.getFields()) + resolveType: def.resolveType + ? def.resolveType + : (source, context, info) => { + const properties = Object.keys(source) + // Remove custom _openAPIToGraphQL property used to pass data + .filter((property) => property !== '_openAPIToGraphQL') + + /** + * Find appropriate member type + * + * TODO: currently, the check is performed by only checking the property + * names. In the future, we should also check the types of those + * properties. + * + * TODO: there is a chance a that an intended member type cannot be + * identified if, for whatever reason, the return data is a superset + * of the fields specified in the OAS + */ + return types.find((type) => { + const typeFields = Object.keys(type.getFields()) + + // The type should be a superset of the properties + if (properties.length <= typeFields.length) { + return properties.every((property) => + typeFields.includes(property) + ) + } - // The type should be a superset of the properties - if (properties.length <= typeFields.length) { - return properties.every((property) => typeFields.includes(property)) + return false + }) } - - return false - }) - } }) return def.graphQLType diff --git a/packages/openapi-to-graphql/src/types/oas3.ts b/packages/openapi-to-graphql/src/types/oas3.ts index d254b049..e9cbd424 100644 --- a/packages/openapi-to-graphql/src/types/oas3.ts +++ b/packages/openapi-to-graphql/src/types/oas3.ts @@ -34,6 +34,10 @@ export type SchemaObject = { anyOf?: (SchemaObject | ReferenceObject)[] oneOf?: (SchemaObject | ReferenceObject)[] not?: (SchemaObject | ReferenceObject)[] + discriminator?: { + propertyName: string + mapping: Map + } } export type ReferenceObject = { diff --git a/packages/openapi-to-graphql/src/types/operation.ts b/packages/openapi-to-graphql/src/types/operation.ts index ac22a0ea..c7878fc4 100644 --- a/packages/openapi-to-graphql/src/types/operation.ts +++ b/packages/openapi-to-graphql/src/types/operation.ts @@ -87,6 +87,12 @@ export type DataDefinition = { // The GraphQL input object type if it is created graphQLInputObjectType?: GraphQLInputObjectType | GraphQLList + + // The potential override of the resolveType function if the Graph QL type is union + resolveType?: any + + // The field for storing object that describes union type that should be used for respose types + unionDefinition?: DataDefinition } export type Operation = { diff --git a/packages/openapi-to-graphql/test/example_api8.test.ts b/packages/openapi-to-graphql/test/example_api8.test.ts new file mode 100644 index 00000000..f889c8a9 --- /dev/null +++ b/packages/openapi-to-graphql/test/example_api8.test.ts @@ -0,0 +1,306 @@ +// Copyright IBM Corp. 2017,2018. All Rights Reserved. +// Node module: openapi-to-graphql +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +'use strict' + +import { graphql } from 'graphql' +import { afterAll, beforeAll, expect, test } from '@jest/globals' + +import * as openAPIToGraphQL from '../lib/index' + +const api = require('./example_api8_server') + +const oas = require('./fixtures/example_oas8.json') + +const PORT = 3004 +// Update PORT for this test case: +oas.servers[0].variables.port.default = String(PORT) + +/** + * This test suite is used to verify the behavior of interOAS links, i.e. + * links across different OASs + */ + +let createdSchema + +/** + * Set up the schema first and run example API server + */ +beforeAll(() => { + return Promise.all([ + openAPIToGraphQL.createGraphQLSchema([oas]).then(({ schema, report }) => { + createdSchema = schema + }), + api.startServer(PORT) + ]) +}) + +/** + * Shut down API server + */ +afterAll(() => { + return Promise.all([api.stopServer()]) +}) + +test('Basic query for types list', () => { + const query = `query { + typesList { + __typename + ... on FirstDerivedType { + name, + baseAttribute, + kind, + firstDerivedTypeAttribute + }, + ... on SecondDerivedType { + name, + baseAttribute, + kind, + secondDerivedTypeAttribute + } + } + }` + return graphql(createdSchema, query).then((result) => { + expect(result).toEqual({ + data: { + typesList: [ + { + baseAttribute: 'authorBaseAttributeValue', + kind: 'FIRST_DERIVED_TYPE', + firstDerivedTypeAttribute: 1, + name: 'author', + __typename: 'FirstDerivedType' + }, + { + baseAttribute: 'bookBaseAttributeValue', + kind: 'SECOND_DERIVED_TYPE', + secondDerivedTypeAttribute: 'listOfBooks', + name: 'book', + __typename: 'SecondDerivedType' + } + ] + } + }) + }) +}) + +test('Query single union type with discriminator', () => { + const query = `query { + firstDerivedType: baseType(typeName: "author") { + ... on FirstDerivedType { + name, + kind, + firstDerivedTypeAttribute + }, + ... on SecondDerivedType { + name, + kind, + secondDerivedTypeAttribute + } + }, + secondDerivedType: baseType(typeName: "book") { + ... on FirstDerivedType { + name, + kind, + firstDerivedTypeAttribute + }, + ... on SecondDerivedType { + name, + kind, + secondDerivedTypeAttribute + } + } + }` + + return graphql(createdSchema, query).then((result) => { + expect(result).toEqual({ + data: { + firstDerivedType: { + kind: 'FIRST_DERIVED_TYPE', + firstDerivedTypeAttribute: 1, + name: 'author' + }, + secondDerivedType: { + kind: 'SECOND_DERIVED_TYPE', + secondDerivedTypeAttribute: 'listOfBooks', + name: 'book' + } + } + }) + }) +}) + +test('Querty with FirstDerivedType in response schema', () => { + const query = `query { + firstDerivedType { + kind, + firstDerivedTypeAttribute, + name, + baseAttribute + } + }` + + return graphql(createdSchema, query).then((result) => { + expect(result).toEqual({ + data: { + firstDerivedType: { + baseAttribute: 'authorBaseAttributeValue', + kind: 'FIRST_DERIVED_TYPE', + firstDerivedTypeAttribute: 1, + name: 'author' + } + } + }) + }) +}) + +test('Query with SecondDerivedType in response schema', () => { + const query = `query { + secondDerivedType { + kind, + secondDerivedTypeAttribute, + name, + baseAttribute + } + }` + + return graphql(createdSchema, query).then((result) => { + expect(result).toEqual({ + data: { + secondDerivedType: { + baseAttribute: 'bookBaseAttributeValue', + kind: 'SECOND_DERIVED_TYPE', + secondDerivedTypeAttribute: 'listOfBooks', + name: 'book' + } + } + }) + }) +}) + +test('Query single union type with oneOf', () => { + const query = `query { + firstDerivedType: oneOfDerivedType(typeName: "author") { + __typename + ... on FirstDerivedType { + name, + kind, + firstDerivedTypeAttribute + }, + ... on SecondDerivedType { + name, + kind, + secondDerivedTypeAttribute + } + }, + secondDerivedType: oneOfDerivedType(typeName: "book") { + __typename + ... on FirstDerivedType { + name, + kind, + firstDerivedTypeAttribute + }, + ... on SecondDerivedType { + name, + kind, + secondDerivedTypeAttribute + } + } + }` + + return graphql(createdSchema, query).then((result) => { + expect(result).toEqual({ + data: { + firstDerivedType: { + kind: 'FIRST_DERIVED_TYPE', + firstDerivedTypeAttribute: 1, + name: 'author', + __typename: 'FirstDerivedType' + }, + secondDerivedType: { + kind: 'SECOND_DERIVED_TYPE', + secondDerivedTypeAttribute: 'listOfBooks', + name: 'book', + __typename: 'SecondDerivedType' + } + } + }) + }) +}) + +test('Create single type', () => { + const mutation = `mutation { + postType(baseTypeCommandInput: { + baseTypeCommandAttribute: "createTypeCommandInput", + type: { + kind: "SECOND_DERIVED_TYPE", + secondDerivedTypeAttribute: "createdBookShopType", + name: "bookShop", + baseAttribute: "createdBookShopBaseAttribute" + } + }) { + data + } + }` + + return graphql(createdSchema, mutation).then((result) => { + expect(result).toEqual({ + data: { + postType: { + data: 'createTypeCommandInput' + } + } + }) + }) +}) + +test('Query list of types with created tpye', () => { + const query = `query { + typesList { + __typename + ... on FirstDerivedType { + name, + baseAttribute, + kind, + firstDerivedTypeAttribute + }, + ... on SecondDerivedType { + name, + baseAttribute, + kind, + secondDerivedTypeAttribute + } + } + }` + return graphql(createdSchema, query).then((result) => { + expect(result).toEqual({ + data: { + typesList: [ + { + baseAttribute: 'authorBaseAttributeValue', + kind: 'FIRST_DERIVED_TYPE', + firstDerivedTypeAttribute: 1, + name: 'author', + __typename: 'FirstDerivedType' + }, + { + baseAttribute: 'bookBaseAttributeValue', + kind: 'SECOND_DERIVED_TYPE', + secondDerivedTypeAttribute: 'listOfBooks', + name: 'book', + __typename: 'SecondDerivedType' + }, + { + baseAttribute: 'createdBookShopBaseAttribute', + kind: 'SECOND_DERIVED_TYPE', + secondDerivedTypeAttribute: 'createdBookShopType', + name: 'bookShop', + __typename: 'SecondDerivedType' + } + ] + } + }) + }) +}) diff --git a/packages/openapi-to-graphql/test/example_api8_server.js b/packages/openapi-to-graphql/test/example_api8_server.js new file mode 100644 index 00000000..576c4973 --- /dev/null +++ b/packages/openapi-to-graphql/test/example_api8_server.js @@ -0,0 +1,106 @@ +// Copyright IBM Corp. 2017,2018. All Rights Reserved. +// Node module: openapi-to-graphql +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +'use strict' + +let server // holds server object for shutdown + +/** + * Starts the server at the given port + */ +function startServer(PORT) { + const express = require('express') + const app = express() + + const bodyParser = require('body-parser') + app.use(bodyParser.text()) + app.use(bodyParser.json()) + + const Types = { + author: { + baseAttribute: 'authorBaseAttributeValue', + kind: 'FIRST_DERIVED_TYPE', + firstDerivedTypeAttribute: 1, + name: 'author' + }, + book: { + baseAttribute: 'bookBaseAttributeValue', + kind: 'SECOND_DERIVED_TYPE', + secondDerivedTypeAttribute: 'listOfBooks', + name: 'book' + } + } + + app.get('/api/type/:typeName', (req, res) => { + res.send(Types[req.params.typeName]) + }) + + app.get('/api/types', (req, res) => { + const t = [] + for (const typeName in Types) { + t.push(Types[typeName]) + } + + res.send(t) + }) + + app.get('/api/firstDerivedType', (req, res) => { + res.send(Types['author']) + }) + + app.get('/api/secondDerivedType', (req, res) => { + res.send(Types['book']) + }) + + app.get('/api/oneOfDerivedTypes/:typeName', (req, res) => { + res.send(Types[req.params.typeName]) + }) + + app.post('/api/type', (req, res) => { + if ( + req.body && + req.body.type && + req.body.type.name && + !Types[req.body.type.name] + ) { + Types[req.body.type.name] = req.body.type + } + + res.send({ + data: req.body.baseTypeCommandAttribute + ? req.body.baseTypeCommandAttribute + : 'created' + }) + }) + + return new Promise((resolve) => { + server = app.listen(PORT, () => { + console.log(`Example API accessible on port ${PORT}`) + resolve() + }) + }) +} + +/** + * Stops server. + */ +function stopServer() { + return new Promise((resolve) => { + server.close(() => { + console.log(`Stopped API server`) + resolve() + }) + }) +} + +// If run from command line, start server: +if (require.main === module) { + startServer(3003) +} + +module.exports = { + startServer, + stopServer +} diff --git a/packages/openapi-to-graphql/test/fixtures/example_oas8.json b/packages/openapi-to-graphql/test/fixtures/example_oas8.json new file mode 100644 index 00000000..20b9a728 --- /dev/null +++ b/packages/openapi-to-graphql/test/fixtures/example_oas8.json @@ -0,0 +1,257 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "Example API 4", + "description": "An API to test converting Open API Specs 3.0 to GraphQL", + "version": "1.0.0", + "termsOfService": "http://example.com/terms/", + "contact": { + "name": "Erik Wittern", + "url": "http://www.example.com/support" + }, + "license": { + "name": "Apache 2.0", + "url": "http://www.apache.org/licenses/LICENSE-2.0.html" + } + }, + "externalDocs": { + "url": "http://example.com/docs", + "description": "Some more natural language description." + }, + "tags": [ + { + "name": "test", + "description": "Indicates this API is for testing" + } + ], + "servers": [ + { + "url": "http://localhost:{port}/{basePath}", + "description": "The location of the local test server.", + "variables": { + "port": { + "default": "3004" + }, + "basePath": { + "default": "api" + } + } + } + ], + "paths": { + "/type/{typeName}": { + "get": { + "description": "Basic union test with discriminator", + "parameters": [ + { + "in": "path", + "name": "typeName", + "schema": { + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/baseType" + } + } + } + } + } + } + }, + "/type": { + "post": { + "description": "Test with nested polymorphic type in the request body", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/baseTypeCommand" + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "string" + } + } + } + } + } + } + } + } + }, + "/types": { + "get": { + "description": "Nested union in response type schema", + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/typesList" + } + } + } + } + } + } + }, + "/firstDerivedType": { + "get": { + "description": "Derived type in the response body", + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/firstDerivedType" + } + } + } + } + } + } + }, + "/secondDerivedType": { + "get": { + "description": "Derived type in the response body", + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/secondDerivedType" + } + } + } + } + } + } + }, + "/oneOfDerivedTypes/{typeName}": { + "get": { + "description": "Nested union in response type", + "parameters": [ + { + "in": "path", + "name": "typeName", + "schema": { + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/firstDerivedType" + }, + { + "$ref": "#/components/schemas/secondDerivedType" + } + ] + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "typesList": { + "type": "array", + "items": { + "$ref": "#/components/schemas/baseType" + } + }, + "baseType": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "baseAttribute": { + "type": "string" + }, + "kind": { + "type": "string", + "enum": ["FIRST_DERIVED_TYPE", "SECOND_DERIVED_TYPE"] + } + }, + "discriminator": { + "propertyName": "kind", + "mapping": { + "FIRST_DERIVED_TYPE": "firstDerivedType", + "SECOND_DERIVED_TYPE": "secondDerivedType" + } + }, + "required": ["kind"] + }, + "baseTypeCommand": { + "type": "object", + "properties": { + "type": { + "$ref": "#/components/schemas/baseType" + }, + "baseTypeCommandAttribute": { + "type": "string" + } + } + }, + "firstDerivedType": { + "allOf": [ + { + "$ref": "#/components/schemas/baseType" + } + ], + "properties": { + "firstDerivedTypeAttribute": { + "type": "number" + } + } + }, + "secondDerivedType": { + "allOf": [ + { + "$ref": "#/components/schemas/baseType" + }, + { + "type": "object", + "properties": { + "secondDerivedTypeAttribute": { + "type": "string" + } + } + } + ] + } + } + } +}