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 :apiwork
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
array :preferences do
reference :preference
end
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(
{ preferences: [
{
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 200
json
{
"preferences": [
{
"address": "alice@example.com",
"channel": "email",
"digest": true
},
{
"channel": "sms",
"phoneNumber": "+1234567890"
},
{
"channel": "push",
"deviceToken": "abc123def456",
"silent": false
}
]
}Exports
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:
preferences:
items:
"$ref": "#/components/schemas/notificationPreference"
type: array
type: object
required:
- preferences
description: ''
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: ''
components:
schemas:
notificationPreference:
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: channelApiwork
json
{
"base_path": "/bright_parrot",
"enums": [],
"error_codes": [],
"fingerprint": "b893ac24db7b822b",
"info": null,
"locales": [],
"resources": [
{
"actions": [
{
"name": "notifications.index",
"deprecated": false,
"description": null,
"method": "get",
"operation_id": null,
"path": "/notifications",
"raises": [],
"request": {
"body": [],
"description": null,
"query": []
},
"response": {
"body": {
"deprecated": null,
"description": null,
"nullable": null,
"optional": null,
"type": "object",
"partial": null,
"shape": [
{
"name": "preferences",
"deprecated": false,
"description": null,
"nullable": false,
"optional": false,
"type": "array",
"example": null,
"max": null,
"min": null,
"of": {
"deprecated": false,
"description": null,
"nullable": false,
"optional": false,
"type": "reference",
"reference": "notification_preference"
}
}
]
},
"description": null,
"no_content": false
},
"summary": null,
"tags": []
},
{
"name": "notifications.create",
"deprecated": false,
"description": null,
"method": "post",
"operation_id": null,
"path": "/notifications",
"raises": [],
"request": {
"body": [
{
"name": "preference",
"deprecated": false,
"description": null,
"nullable": false,
"optional": false,
"type": "union",
"discriminator": "channel",
"variants": [
{
"deprecated": false,
"description": null,
"nullable": false,
"optional": false,
"type": "object",
"partial": false,
"shape": [
{
"name": "address",
"deprecated": false,
"description": null,
"nullable": false,
"optional": false,
"type": "string",
"example": null,
"format": null,
"max": null,
"min": null
},
{
"name": "digest",
"deprecated": false,
"description": null,
"nullable": false,
"optional": false,
"type": "boolean",
"example": null
}
],
"tag": "email"
},
{
"deprecated": false,
"description": null,
"nullable": false,
"optional": false,
"type": "object",
"partial": false,
"shape": [
{
"name": "phoneNumber",
"deprecated": false,
"description": null,
"nullable": false,
"optional": false,
"type": "string",
"example": null,
"format": null,
"max": null,
"min": null
}
],
"tag": "sms"
},
{
"deprecated": false,
"description": null,
"nullable": false,
"optional": false,
"type": "object",
"partial": false,
"shape": [
{
"name": "deviceToken",
"deprecated": false,
"description": null,
"nullable": false,
"optional": false,
"type": "string",
"example": null,
"format": null,
"max": null,
"min": null
},
{
"name": "silent",
"deprecated": false,
"description": null,
"nullable": false,
"optional": false,
"type": "boolean",
"example": null
}
],
"tag": "push"
}
]
}
],
"description": null,
"query": []
},
"response": {
"body": {
"deprecated": null,
"description": null,
"nullable": null,
"optional": null,
"type": "object",
"partial": null,
"shape": [
{
"name": "preference",
"deprecated": false,
"description": null,
"nullable": false,
"optional": false,
"type": "union",
"discriminator": "channel",
"variants": [
{
"deprecated": false,
"description": null,
"nullable": false,
"optional": false,
"type": "object",
"partial": false,
"shape": [
{
"name": "address",
"deprecated": false,
"description": null,
"nullable": false,
"optional": false,
"type": "string",
"example": null,
"format": null,
"max": null,
"min": null
},
{
"name": "digest",
"deprecated": false,
"description": null,
"nullable": false,
"optional": false,
"type": "boolean",
"example": null
}
],
"tag": "email"
},
{
"deprecated": false,
"description": null,
"nullable": false,
"optional": false,
"type": "object",
"partial": false,
"shape": [
{
"name": "phoneNumber",
"deprecated": false,
"description": null,
"nullable": false,
"optional": false,
"type": "string",
"example": null,
"format": null,
"max": null,
"min": null
}
],
"tag": "sms"
},
{
"deprecated": false,
"description": null,
"nullable": false,
"optional": false,
"type": "object",
"partial": false,
"shape": [
{
"name": "deviceToken",
"deprecated": false,
"description": null,
"nullable": false,
"optional": false,
"type": "string",
"example": null,
"format": null,
"max": null,
"min": null
},
{
"name": "silent",
"deprecated": false,
"description": null,
"nullable": false,
"optional": false,
"type": "boolean",
"example": null
}
],
"tag": "push"
}
]
}
]
},
"description": null,
"no_content": false
},
"summary": null,
"tags": []
}
],
"identifier": "notifications",
"name": "notifications",
"parent_identifiers": [],
"path": "notifications",
"resources": [],
"scope": "notification"
}
],
"types": [
{
"recursive": false,
"deprecated": false,
"description": null,
"example": null,
"name": "notification_preference",
"scope": "notification",
"type": "union",
"discriminator": "channel",
"variants": [
{
"deprecated": false,
"description": null,
"nullable": false,
"optional": false,
"type": "object",
"partial": false,
"shape": [
{
"name": "address",
"deprecated": false,
"description": null,
"nullable": false,
"optional": false,
"type": "string",
"example": null,
"format": null,
"max": null,
"min": null
},
{
"name": "digest",
"deprecated": false,
"description": null,
"nullable": false,
"optional": false,
"type": "boolean",
"example": null
}
],
"tag": "email"
},
{
"deprecated": false,
"description": null,
"nullable": false,
"optional": false,
"type": "object",
"partial": false,
"shape": [
{
"name": "phoneNumber",
"deprecated": false,
"description": null,
"nullable": false,
"optional": false,
"type": "string",
"example": null,
"format": null,
"max": null,
"min": null
}
],
"tag": "sms"
},
{
"deprecated": false,
"description": null,
"nullable": false,
"optional": false,
"type": "object",
"partial": false,
"shape": [
{
"name": "deviceToken",
"deprecated": false,
"description": null,
"nullable": false,
"optional": false,
"type": "string",
"example": null,
"format": null,
"max": null,
"min": null
},
{
"name": "silent",
"deprecated": false,
"description": null,
"nullable": false,
"optional": false,
"type": "boolean",
"example": null
}
],
"tag": "push"
}
]
}
]
}Codegen
TypeScript
ts
export * from './notification';ts
export type NotificationPreference =
| ({ address: string; digest: boolean } & { channel: 'email' })
| ({ phoneNumber: string } & { channel: 'sms' })
| ({ deviceToken: string; silent: boolean } & { channel: 'push' });ts
export * from './notifications';ts
import type { NotificationPreference } from '../domains/notification';
export type NotificationsIndexMethod = 'GET';
export type NotificationsIndexPath = '/notifications';
export type NotificationsIndexResponseBody = {
preferences: NotificationPreference[];
};
export interface NotificationsIndexResponse {
body: NotificationsIndexResponseBody;
}
export interface NotificationsIndex {
method: NotificationsIndexMethod;
path: NotificationsIndexPath;
response: NotificationsIndexResponse;
}
export type NotificationsCreateMethod = 'POST';
export type NotificationsCreatePath = '/notifications';
export interface NotificationsCreateRequestBody {
preference:
| { address: string; digest: boolean }
| { phoneNumber: string }
| { deviceToken: string; silent: boolean };
}
export type NotificationsCreateResponseBody = {
preference:
| { address: string; digest: boolean }
| { phoneNumber: string }
| { deviceToken: string; silent: boolean };
};
export interface NotificationsCreateRequest {
body: NotificationsCreateRequestBody;
}
export interface NotificationsCreateResponse {
body: NotificationsCreateResponseBody;
}
export interface NotificationsCreate {
method: NotificationsCreateMethod;
path: NotificationsCreatePath;
request: NotificationsCreateRequest;
response: NotificationsCreateResponse;
}Zod
ts
export * from './notification';ts
import * as z from 'zod';
export const NotificationPreferenceSchema = z.discriminatedUnion('channel', [
z
.object({ address: z.string(), digest: z.boolean() })
.extend({ channel: z.literal('email') }),
z.object({ phoneNumber: z.string() }).extend({ channel: z.literal('sms') }),
z
.object({ deviceToken: z.string(), silent: z.boolean() })
.extend({ channel: z.literal('push') }),
]);
export type NotificationPreference =
| ({ address: string; digest: boolean } & { channel: 'email' })
| ({ phoneNumber: string } & { channel: 'sms' })
| ({ deviceToken: string; silent: boolean } & { channel: 'push' });ts
export * from './notifications';ts
import type { NotificationPreference } from '../domains/notification';
import * as z from 'zod';
import { NotificationPreferenceSchema } from '../domains/notification';
export const NotificationsIndexResponseBodySchema = z.object({
preferences: z.array(NotificationPreferenceSchema),
});
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 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 type NotificationsIndexMethod = 'GET';
export type NotificationsIndexPath = '/notifications';
export type NotificationsIndexResponseBody = {
preferences: NotificationPreference[];
};
export interface NotificationsIndexResponse {
body: NotificationsIndexResponseBody;
}
export interface NotificationsIndex {
method: NotificationsIndexMethod;
path: NotificationsIndexPath;
response: NotificationsIndexResponse;
}
export type NotificationsCreateMethod = 'POST';
export type NotificationsCreatePath = '/notifications';
export interface NotificationsCreateRequestBody {
preference:
| { address: string; digest: boolean }
| { phoneNumber: string }
| { deviceToken: string; silent: boolean };
}
export type NotificationsCreateResponseBody = {
preference:
| { address: string; digest: boolean }
| { phoneNumber: string }
| { deviceToken: string; silent: boolean };
};
export interface NotificationsCreateRequest {
body: NotificationsCreateRequestBody;
}
export interface NotificationsCreateResponse {
body: NotificationsCreateResponseBody;
}
export interface NotificationsCreate {
method: NotificationsCreateMethod;
path: NotificationsCreatePath;
request: NotificationsCreateRequest;
response: NotificationsCreateResponse;
}Sorbus
ts
import type { NotificationsOperationTree } from './endpoints';
import { createClientFactory } from 'sorbus';
import { contract } from './contract';
export interface Client {
notifications: NotificationsOperationTree;
}
export const createClient = createClientFactory<Client>(contract);ts
import { notifications } from './endpoints';
export const contract = {
endpoints: {
notifications,
},
} as const;ts
export * from './notification';ts
import * as z from 'zod';
export const NotificationPreferenceSchema = z.discriminatedUnion('channel', [
z
.object({ address: z.string(), digest: z.boolean() })
.extend({ channel: z.literal('email') }),
z.object({ phoneNumber: z.string() }).extend({ channel: z.literal('sms') }),
z
.object({ deviceToken: z.string(), silent: z.boolean() })
.extend({ channel: z.literal('push') }),
]);
export type NotificationPreference =
| ({ address: string; digest: boolean } & { channel: 'email' })
| ({ phoneNumber: string } & { channel: 'sms' })
| ({ deviceToken: string; silent: boolean } & { channel: 'push' });ts
export * from './notifications';ts
import type { Operation } from 'sorbus';
import type { NotificationPreference } from '../domains/notification';
import * as z from 'zod';
import { NotificationPreferenceSchema } from '../domains/notification';
export const NotificationsIndexResponseBodySchema = z.object({
preferences: z.array(NotificationPreferenceSchema),
});
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 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 type NotificationsIndexMethod = 'GET';
export type NotificationsIndexPath = '/notifications';
export type NotificationsIndexResponseBody = {
preferences: NotificationPreference[];
};
export interface NotificationsIndexResponse {
body: NotificationsIndexResponseBody;
}
export interface NotificationsIndex {
method: NotificationsIndexMethod;
path: NotificationsIndexPath;
response: NotificationsIndexResponse;
}
export type NotificationsCreateMethod = 'POST';
export type NotificationsCreatePath = '/notifications';
export interface NotificationsCreateRequestBody {
preference:
| { address: string; digest: boolean }
| { phoneNumber: string }
| { deviceToken: string; silent: boolean };
}
export type NotificationsCreateResponseBody = {
preference:
| { address: string; digest: boolean }
| { phoneNumber: string }
| { deviceToken: string; silent: boolean };
};
export interface NotificationsCreateRequest {
body: NotificationsCreateRequestBody;
}
export interface NotificationsCreateResponse {
body: NotificationsCreateResponseBody;
}
export interface NotificationsCreate {
method: NotificationsCreateMethod;
path: NotificationsCreatePath;
request: NotificationsCreateRequest;
response: NotificationsCreateResponse;
}
export const notifications = {
create: {
method: 'POST',
path: '/notifications',
request: {
body: NotificationsCreateRequestBodySchema,
},
response: {
body: NotificationsCreateResponseBodySchema,
},
},
index: {
method: 'GET',
path: '/notifications',
response: {
body: NotificationsIndexResponseBodySchema,
},
},
} as const;
export interface NotificationsOperationTree {
create: Operation<NotificationsCreate>;
index: Operation<NotificationsIndex>;
}