Union Types
Discriminated unions for polymorphic request and response shapes
API Definition
config/apis/bright_parrot.rb
rb
# frozen_string_literal: true
Apiwork::API.define '/bright_parrot' do
key_format :camel
export :openapi
export :typescript
export :zod
resources :notifications, only: %i[index create]
endContracts
app/contracts/bright_parrot/notification_contract.rb
rb
# frozen_string_literal: true
module BrightParrot
class NotificationContract < Apiwork::Contract::Base
union :preference, discriminator: :channel do
variant tag: 'email' do
object do
string :address
boolean :digest
end
end
variant tag: 'sms' do
object do
string :phone_number
end
end
variant tag: 'push' do
object do
string :device_token
boolean :silent
end
end
end
action :create do
request do
body do
reference :preference
end
end
response do
body do
reference :preference
end
end
end
action :index do
response do
body do
reference :preference
end
end
end
end
endControllers
app/controllers/bright_parrot/notifications_controller.rb
rb
# frozen_string_literal: true
module BrightParrot
class NotificationsController < ApplicationController
def index
expose(
[
{
address: 'alice@example.com',
channel: 'email',
digest: true,
},
{
channel: 'sms',
phone_number: '+1234567890',
},
{
channel: 'push',
device_token: 'abc123def456',
silent: false,
},
],
)
end
def create
expose(contract.body[:preference])
end
end
endRequest Examples
Create email preference
Request
http
POST /bright_parrot/notifications
Content-Type: application/json
{
"preference": {
"channel": "email",
"address": "alice@example.com",
"digest": true
}
}Response 201
json
{
"address": "alice@example.com",
"digest": true,
"channel": "email"
}Create SMS preference
Request
http
POST /bright_parrot/notifications
Content-Type: application/json
{
"preference": {
"channel": "sms",
"phoneNumber": "+1234567890"
}
}Response 201
json
{
"phoneNumber": "+1234567890",
"channel": "sms"
}List preferences
Request
http
GET /bright_parrot/notificationsResponse 500
json
{
"status": 500,
"error": "Internal Server Error"
}Generated Output
Introspection
json
{
"base_path": "/bright_parrot",
"enums": {
"layer": {
"deprecated": false,
"description": null,
"example": null,
"values": [
"http",
"contract",
"domain"
]
}
},
"error_codes": {},
"info": null,
"resources": {
"notifications": {
"actions": {
"index": {
"deprecated": false,
"description": null,
"method": "get",
"operation_id": null,
"path": "/notifications",
"raises": [],
"request": {
"body": {},
"query": {}
},
"response": {
"body": {
"default": null,
"deprecated": null,
"description": null,
"example": null,
"nullable": null,
"optional": null,
"type": "object",
"partial": null,
"shape": {
"preference": {
"default": null,
"deprecated": false,
"description": null,
"example": null,
"nullable": false,
"optional": false,
"type": "union",
"discriminator": "channel",
"variants": [
{
"default": null,
"deprecated": false,
"description": null,
"example": null,
"nullable": false,
"optional": false,
"type": "object",
"partial": false,
"shape": {
"address": {
"default": null,
"deprecated": false,
"description": null,
"example": null,
"nullable": false,
"optional": false,
"type": "string",
"format": null,
"max": null,
"min": null
},
"digest": {
"default": null,
"deprecated": false,
"description": null,
"example": null,
"nullable": false,
"optional": false,
"type": "boolean"
}
}
},
{
"default": null,
"deprecated": false,
"description": null,
"example": null,
"nullable": false,
"optional": false,
"type": "object",
"partial": false,
"shape": {
"phone_number": {
"default": null,
"deprecated": false,
"description": null,
"example": null,
"nullable": false,
"optional": false,
"type": "string",
"format": null,
"max": null,
"min": null
}
}
},
{
"default": null,
"deprecated": false,
"description": null,
"example": null,
"nullable": false,
"optional": false,
"type": "object",
"partial": false,
"shape": {
"device_token": {
"default": null,
"deprecated": false,
"description": null,
"example": null,
"nullable": false,
"optional": false,
"type": "string",
"format": null,
"max": null,
"min": null
},
"silent": {
"default": null,
"deprecated": false,
"description": null,
"example": null,
"nullable": false,
"optional": false,
"type": "boolean"
}
}
}
]
}
}
},
"no_content": false
},
"summary": null,
"tags": []
},
"create": {
"deprecated": false,
"description": null,
"method": "post",
"operation_id": null,
"path": "/notifications",
"raises": [],
"request": {
"body": {
"preference": {
"default": null,
"deprecated": false,
"description": null,
"example": null,
"nullable": false,
"optional": false,
"type": "union",
"discriminator": "channel",
"variants": [
{
"default": null,
"deprecated": false,
"description": null,
"example": null,
"nullable": false,
"optional": false,
"type": "object",
"partial": false,
"shape": {
"address": {
"default": null,
"deprecated": false,
"description": null,
"example": null,
"nullable": false,
"optional": false,
"type": "string",
"format": null,
"max": null,
"min": null
},
"digest": {
"default": null,
"deprecated": false,
"description": null,
"example": null,
"nullable": false,
"optional": false,
"type": "boolean"
}
}
},
{
"default": null,
"deprecated": false,
"description": null,
"example": null,
"nullable": false,
"optional": false,
"type": "object",
"partial": false,
"shape": {
"phone_number": {
"default": null,
"deprecated": false,
"description": null,
"example": null,
"nullable": false,
"optional": false,
"type": "string",
"format": null,
"max": null,
"min": null
}
}
},
{
"default": null,
"deprecated": false,
"description": null,
"example": null,
"nullable": false,
"optional": false,
"type": "object",
"partial": false,
"shape": {
"device_token": {
"default": null,
"deprecated": false,
"description": null,
"example": null,
"nullable": false,
"optional": false,
"type": "string",
"format": null,
"max": null,
"min": null
},
"silent": {
"default": null,
"deprecated": false,
"description": null,
"example": null,
"nullable": false,
"optional": false,
"type": "boolean"
}
}
}
]
}
},
"query": {}
},
"response": {
"body": {
"default": null,
"deprecated": null,
"description": null,
"example": null,
"nullable": null,
"optional": null,
"type": "object",
"partial": null,
"shape": {
"preference": {
"default": null,
"deprecated": false,
"description": null,
"example": null,
"nullable": false,
"optional": false,
"type": "union",
"discriminator": "channel",
"variants": [
{
"default": null,
"deprecated": false,
"description": null,
"example": null,
"nullable": false,
"optional": false,
"type": "object",
"partial": false,
"shape": {
"address": {
"default": null,
"deprecated": false,
"description": null,
"example": null,
"nullable": false,
"optional": false,
"type": "string",
"format": null,
"max": null,
"min": null
},
"digest": {
"default": null,
"deprecated": false,
"description": null,
"example": null,
"nullable": false,
"optional": false,
"type": "boolean"
}
}
},
{
"default": null,
"deprecated": false,
"description": null,
"example": null,
"nullable": false,
"optional": false,
"type": "object",
"partial": false,
"shape": {
"phone_number": {
"default": null,
"deprecated": false,
"description": null,
"example": null,
"nullable": false,
"optional": false,
"type": "string",
"format": null,
"max": null,
"min": null
}
}
},
{
"default": null,
"deprecated": false,
"description": null,
"example": null,
"nullable": false,
"optional": false,
"type": "object",
"partial": false,
"shape": {
"device_token": {
"default": null,
"deprecated": false,
"description": null,
"example": null,
"nullable": false,
"optional": false,
"type": "string",
"format": null,
"max": null,
"min": null
},
"silent": {
"default": null,
"deprecated": false,
"description": null,
"example": null,
"nullable": false,
"optional": false,
"type": "boolean"
}
}
}
]
}
}
},
"no_content": false
},
"summary": null,
"tags": []
}
},
"identifier": "notifications",
"parent_identifiers": [],
"path": "notifications",
"resources": {}
}
},
"types": {
"error": {
"deprecated": false,
"description": null,
"discriminator": null,
"example": null,
"extends": [],
"shape": {
"issues": {
"default": null,
"deprecated": false,
"description": null,
"example": null,
"nullable": false,
"optional": false,
"type": "array",
"max": null,
"min": null,
"of": {
"default": null,
"deprecated": null,
"description": null,
"example": null,
"nullable": null,
"optional": null,
"type": "reference",
"reference": "issue"
},
"shape": {}
},
"layer": {
"default": null,
"deprecated": false,
"description": null,
"example": null,
"nullable": false,
"optional": false,
"type": "reference",
"reference": "layer"
}
},
"type": "object",
"variants": []
},
"error_response_body": {
"deprecated": false,
"description": null,
"discriminator": null,
"example": null,
"extends": [
"error"
],
"shape": {},
"type": "object",
"variants": []
},
"issue": {
"deprecated": false,
"description": null,
"discriminator": null,
"example": null,
"extends": [],
"shape": {
"code": {
"default": null,
"deprecated": false,
"description": null,
"example": null,
"nullable": false,
"optional": false,
"type": "string",
"format": null,
"max": null,
"min": null
},
"detail": {
"default": null,
"deprecated": false,
"description": null,
"example": null,
"nullable": false,
"optional": false,
"type": "string",
"format": null,
"max": null,
"min": null
},
"meta": {
"default": null,
"deprecated": false,
"description": null,
"example": null,
"nullable": false,
"optional": false,
"type": "object",
"partial": false,
"shape": {}
},
"path": {
"default": null,
"deprecated": false,
"description": null,
"example": null,
"nullable": false,
"optional": false,
"type": "array",
"max": null,
"min": null,
"of": {
"default": null,
"deprecated": null,
"description": null,
"example": null,
"nullable": null,
"optional": null,
"type": "string",
"format": null,
"max": null,
"min": null
},
"shape": {}
},
"pointer": {
"default": null,
"deprecated": false,
"description": null,
"example": null,
"nullable": false,
"optional": false,
"type": "string",
"format": null,
"max": null,
"min": null
}
},
"type": "object",
"variants": []
},
"notification_preference": {
"deprecated": false,
"description": null,
"discriminator": "channel",
"example": null,
"extends": [],
"shape": {},
"type": "union",
"variants": [
{
"default": null,
"deprecated": false,
"description": null,
"example": null,
"nullable": false,
"optional": false,
"type": "object",
"partial": false,
"shape": {
"address": {
"default": null,
"deprecated": false,
"description": null,
"example": null,
"nullable": false,
"optional": false,
"type": "string",
"format": null,
"max": null,
"min": null
},
"digest": {
"default": null,
"deprecated": false,
"description": null,
"example": null,
"nullable": false,
"optional": false,
"type": "boolean"
}
}
},
{
"default": null,
"deprecated": false,
"description": null,
"example": null,
"nullable": false,
"optional": false,
"type": "object",
"partial": false,
"shape": {
"phone_number": {
"default": null,
"deprecated": false,
"description": null,
"example": null,
"nullable": false,
"optional": false,
"type": "string",
"format": null,
"max": null,
"min": null
}
}
},
{
"default": null,
"deprecated": false,
"description": null,
"example": null,
"nullable": false,
"optional": false,
"type": "object",
"partial": false,
"shape": {
"device_token": {
"default": null,
"deprecated": false,
"description": null,
"example": null,
"nullable": false,
"optional": false,
"type": "string",
"format": null,
"max": null,
"min": null
},
"silent": {
"default": null,
"deprecated": false,
"description": null,
"example": null,
"nullable": false,
"optional": false,
"type": "boolean"
}
}
}
]
}
}
}TypeScript
ts
export interface NotificationsCreateRequest {
body: NotificationsCreateRequestBody;
}
export interface NotificationsCreateRequestBody {
preference: { address: string; digest: boolean } | { deviceToken: string; silent: boolean } | { phoneNumber: string };
}
export interface NotificationsCreateResponse {
body: NotificationsCreateResponseBody;
}
export type NotificationsCreateResponseBody = { preference: { address: string; digest: boolean } | { deviceToken: string; silent: boolean } | { phoneNumber: string } };
export interface NotificationsIndexResponse {
body: NotificationsIndexResponseBody;
}
export type NotificationsIndexResponseBody = { preference: { address: string; digest: boolean } | { deviceToken: string; silent: boolean } | { phoneNumber: string } };Zod
ts
import { z } from 'zod';
export const NotificationsIndexResponseBodySchema = z.object({ preference: z.discriminatedUnion('channel', [z.object({ address: z.string(), digest: z.boolean() }), z.object({ phoneNumber: z.string() }), z.object({ deviceToken: z.string(), silent: z.boolean() })]) });
export const NotificationsIndexResponseSchema = z.object({
body: NotificationsIndexResponseBodySchema
});
export const NotificationsCreateRequestBodySchema = z.object({
preference: z.discriminatedUnion('channel', [z.object({ address: z.string(), digest: z.boolean() }), z.object({ phoneNumber: z.string() }), z.object({ deviceToken: z.string(), silent: z.boolean() })])
});
export const NotificationsCreateRequestSchema = z.object({
body: NotificationsCreateRequestBodySchema
});
export const NotificationsCreateResponseBodySchema = z.object({ preference: z.discriminatedUnion('channel', [z.object({ address: z.string(), digest: z.boolean() }), z.object({ phoneNumber: z.string() }), z.object({ deviceToken: z.string(), silent: z.boolean() })]) });
export const NotificationsCreateResponseSchema = z.object({
body: NotificationsCreateResponseBodySchema
});
export interface NotificationsCreateRequest {
body: NotificationsCreateRequestBody;
}
export interface NotificationsCreateRequestBody {
preference: { address: string; digest: boolean } | { deviceToken: string; silent: boolean } | { phoneNumber: string };
}
export interface NotificationsCreateResponse {
body: NotificationsCreateResponseBody;
}
export type NotificationsCreateResponseBody = { preference: { address: string; digest: boolean } | { deviceToken: string; silent: boolean } | { phoneNumber: string } };
export interface NotificationsIndexResponse {
body: NotificationsIndexResponseBody;
}
export type NotificationsIndexResponseBody = { preference: { address: string; digest: boolean } | { deviceToken: string; silent: boolean } | { phoneNumber: string } };OpenAPI
yml
---
openapi: 3.1.0
info:
title: "/bright_parrot"
version: 1.0.0
paths:
"/notifications":
get:
operationId: notificationsIndex
responses:
'200':
content:
application/json:
schema:
properties:
preference:
oneOf:
- allOf:
- properties:
address:
type: string
digest:
type: boolean
type: object
required:
- address
- digest
- properties:
channel:
const: email
type: string
required:
- channel
type: object
- allOf:
- properties:
phoneNumber:
type: string
type: object
required:
- phoneNumber
- properties:
channel:
const: sms
type: string
required:
- channel
type: object
- allOf:
- properties:
deviceToken:
type: string
silent:
type: boolean
type: object
required:
- deviceToken
- silent
- properties:
channel:
const: push
type: string
required:
- channel
type: object
discriminator:
propertyName: channel
type: object
required:
- preference
description: Successful response
post:
operationId: notificationsCreate
requestBody:
content:
application/json:
schema:
properties:
preference:
oneOf:
- allOf:
- properties:
address:
type: string
digest:
type: boolean
type: object
required:
- address
- digest
- properties:
channel:
const: email
type: string
required:
- channel
type: object
- allOf:
- properties:
phoneNumber:
type: string
type: object
required:
- phoneNumber
- properties:
channel:
const: sms
type: string
required:
- channel
type: object
- allOf:
- properties:
deviceToken:
type: string
silent:
type: boolean
type: object
required:
- deviceToken
- silent
- properties:
channel:
const: push
type: string
required:
- channel
type: object
discriminator:
propertyName: channel
type: object
required:
- preference
required: true
responses:
'200':
content:
application/json:
schema:
properties:
preference:
oneOf:
- allOf:
- properties:
address:
type: string
digest:
type: boolean
type: object
required:
- address
- digest
- properties:
channel:
const: email
type: string
required:
- channel
type: object
- allOf:
- properties:
phoneNumber:
type: string
type: object
required:
- phoneNumber
- properties:
channel:
const: sms
type: string
required:
- channel
type: object
- allOf:
- properties:
deviceToken:
type: string
silent:
type: boolean
type: object
required:
- deviceToken
- silent
- properties:
channel:
const: push
type: string
required:
- channel
type: object
discriminator:
propertyName: channel
type: object
required:
- preference
description: Successful response
components:
schemas: {}