Swiftpack.co -  mattpolzin/OpenAPIKit as Swift Package
Swiftpack.co is a collection of thousands of indexed Swift packages. Search packages.
mattpolzin/OpenAPIKit
Codable Swift OpenAPI implementation.
.package(url: "https://github.com/mattpolzin/OpenAPIKit.git", from: "3.0.0-alpha.1")

sswg:sandbox|94x20 Swift 5.1+

MIT license Tests

OpenAPIKit

A library containing Swift types that encode to- and decode from OpenAPI Documents and their components.

Usage

If you are migrating from OpenAPIKit 1.x to OpenAPIKit 2.x, check out the migration guide.

Decoding OpenAPI Documents

You can decode a JSON OpenAPI document (i.e. using the JSONDecoder from Foundation library) or a YAML OpenAPI document (i.e. using the YAMLDecoder from the Yams library) with the following code:

let decoder = ... // JSONDecoder() or YAMLDecoder()
let openAPIDoc = try decoder.decode(OpenAPI.Document.self, from: ...)

Decoding Errors

You can wrap any error you get back from a decoder in OpenAPI.Error to get a friendlier human-readable description from localizedDescription.

do {
  try decoder.decode(OpenAPI.Document.self, from: ...)
} catch let error {
  print(OpenAPI.Error(from: error).localizedDescription)  
}

Encoding OpenAPI Documents

You can encode a JSON OpenAPI document (i.e. using the JSONEncoder from the Foundation library) or a YAML OpenAPI document (i.e. using the YAMLEncoder from the Yams library) with the following code:

let openAPIDoc = ...
let encoder = ... // JSONEncoder() or YAMLEncoder()
let encodedOpenAPIDoc = try encoder.encode(openAPIDoc)

Validating OpenAPI Documents

Thanks to Swift's type system, the vast majority of the OpenAPI Specification is represented by the types of OpenAPIKit -- you cannot create bad OpenAPI docuements in the first place and decoding a document will fail with generally useful errors.

That being said, there are a small number of additional checks that you can perform to really put any concerns to bed.

let openAPIDoc = ...
// perform additional validations on the document:
try openAPIDoc.validate()

You can use this same validation system to dig arbitrarily deep into an OpenAPI Document and assert things that the OpenAPI Specification does not actually mandate. For more on validation, see the OpenAPIKit Validation Documentation.

A note on dictionary ordering

The Foundation library's JSONEncoder and JSONDecoder do not make any guarantees about the ordering of keyed containers. This means decoding a JSON OpenAPI Document and then encoding again might result in the document's various hashed structures being in a different order.

If retaining order is important for your use-case, I recommend the Yams and FineJSON libraries for YAML and JSON respectively. Also keep in mind that JSON is entirely valid YAML and therefore you will likely get good results from Yams decoding of JSON as well (it just won't encode as valid JSON).

The Foundation JSON encoding and decoding will be the most stable and battle-tested option with Yams as a pretty well established and stable option as well. FineJSON is lesser used (to my knowledge) but I have had success with it in the past.

OpenAPI Document structure

The types used by this library largely mirror the object definitions found in the OpenAPI specification version 3.0.3. The Project Status lists each object defined by the spec and the name of the respective type in this library.

Document Root

At the root there is an OpenAPI.Document. In addition to some information that applies to the entire API, the document contains OpenAPI.Components (essentially a dictionary of reusable components that can be referenced with JSONReferences) and an OpenAPI.PathItem.Map (a dictionary of routes your API defines).

Routes

Each route is an entry in the document's OpenAPI.PathItem.Map. The keys of this dictionary are the paths for each route (i.e. /widgets). The values of this dictionary are OpenAPI.PathItems which define any combination of endpoints (i.e. GET, POST, PATCH, etc.) that the given route supports. In addition to accessing endpoints on a path item under the name of the method (.get, .post, etc.), you can get an array of pairs matching endpoint methods to operations with the .endpoints method on PathItem.

Endpoints

Each endpoint on a route is defined by an OpenAPI.Operation. Among other things, this operation can specify the parameters (path, query, header, etc.), request body, and response bodies/codes supported by the given endpoint.

Request/Response Bodies

Request and response bodies can be defined in great detail using OpenAPI's derivative of the JSON Schema specification. This library uses the JSONSchema type for such schema definitions.

Schemas

Fundamental types are specified as JSONSchema.integer, JSONSchema.string, JSONSchema.boolean, etc.

Schema attributes are given as arguments to static constructors. By default, schemas are non-nullable, required, and generic. The below examples are not comprehensive and you can pass any number of the optional attributes to the static constructors as arguments.

A schema can be made optional (i.e. it can be omitted) with JSONSchema.integer(required: false) or an existing schema can be asked for an optionalSchemaObject().

A schema can be made nullable with JSONSchema.number(nullable: true) or an existing schema can be asked for a nullableSchemaObject().

Some types of schemas can be further specialized with a format. For example, JSONSchema.number(format: .double) or JSONSchema.string(format: .dateTime).

You can specify a schema's allowed values (e.g. for an enumerated type) with JSONSchema.string(allowedValues: "hello", "world").

Each type of schema has its own additional set of properties that can be specified. For example, integers can have a minimum value: JSONSchema.integer(minimum: (0, exclusive: true)). exclusive: true in this context means the number must be strictly greater than 0 whereas exclusive: false means the number must be greater-than or equal-to 0.

Compound objects can be built with JSONSchema.array, JSONSchema.object, JSONSchema.all(of:), etc.

For example, perhaps a person is represented by the schema:

JSONSchema.object(
  title: "Person",
  properties: [
    "first_name": .string(minLength: 2),
    "last_name": .string(nullable: true),
    "age": .integer,
    "favorite_color": .string(allowedValues: "red", "green", "blue")
  ]
)

Take a look at the OpenAPIKit Schema Object documentation for more information.

JSON References

The JSONReference type allows you to work with OpenAPIDocuments that store some of their information in the shared Components Object dictionary or even external files. Only documents where all references point to the Components Object can be dereferenced currently, but you can encode and decode all references.

You can create an external reference with JSONReference.external(URL). Internal references usually refer to an object in the Components Object dictionary and are constructed with JSONReference.component(named:). If you need to refer to something in the current file but not in the Components Object, you can use JSONReference.internal(path:).

You can check whether a given JSONReference exists in the Components Object with document.components.contains(). You can access a referenced object in the Components Object with document.components[reference].

You can create references from the Components Object with document.components.reference(named:ofType:). This method will throw an error if the given component does not exist in the ComponentsObject.

You can use document.components.lookup() or the Components type's subscript to turn an Either containing either a reference or a component into an optional value of that component's type (having either pulled it out of the Either or looked it up in the Components Object). The lookup() method throws when it can't find an item whereas subscript returns nil.

For example,

let apiDoc: OpenAPI.Document = ...
let addBooksPath = apiDoc.paths["/cloudloading/addBook"]

let addBooksParameters: [OpenAPI.Parameter]? = addBooksPath?.parameters.compactMap { apiDoc.components[$0] }

Note that this looks a component up in the Components Object but it does not transform it into an entirely derefernced object in the same way as is described below in the Dereferencing & Resolving section.

Security Requirements

In the OpenAPI specifcation, a security requirement (like can be found on the root Document or on Operations) is a dictionary where each key is the name of a security scheme found in the Components Object and each value is an array of applicable scopes (which is of course only a non-empty array when the security scheme type is one for which "scopes" are relevant).

OpenAPIKit defines the SecurityRequirement typealias as a dictionary with JSONReference keys; These references point to the Components Object and provide a slightly stronger contract than the String values required by the OpenAPI specification. Naturally, these are encoded to JSON/YAML as String values rather than JSON References to maintain compliance with the OpenAPI Specification.

Specification Extensions

Many OpenAPIKit types support Specification Extensions. As described in the OpenAPI Specification, these extensions must be objects that are keyed with the prefix "x-". For example, a property named "specialProperty" on the root OpenAPI Object (OpenAPI.Document) is invalid but the property "x-specialProperty" is a valid specification extension.

You can get or set specification extensions via the vendorExtensions property on any object that supports this feature. The keys are Strings beginning with the aforementioned "x-" prefix and the values are AnyCodable. If you set an extension without using the "x-" prefix, the prefix will be added upon encoding.

AnyCodable can be constructed from literals or explicitly. The following are all valid.

var document = OpenAPI.Document(...)

document.vendorExtensions["x-specialProperty1"] = true
document.vendorExtensions["x-specialProperty2"] = "hello world"
document.vendorExtensions["x-specialProperty3"] = ["hello", "world"]
document.vendorExtensions["x-specialProperty4"] = ["hello": "world"]
document.vendorExtensions["x-specialProperty5"] = AnyCodable("hello world")

Dereferencing & Resolving

In addition to looking something up in the Components object, you can entirely derefererence many OpenAPIKit types. A dereferenced type has had all of its references looked up (and all of its properties' references, all the way down).

You use a value's dereferenced(in:) method to fully dereference it.

You can even dereference the whole document with the OpenAPI.Document locallyDereferenced() method. As the name implies, you can only derefence whole documents that are contained within one file (which is another way of saying that all references are "local"). Specifically, all references must be located within the document's Components Object.

Unlike what happens when you lookup an individual component using the lookup() method on Components, dereferencing a whole OpenAPI.Document will result in type-level changes that guarantee all references are removed. OpenAPI.Document's locallyDereferenced() method returns a DereferencedDocument which exposes DereferencedPathItems which have DereferencedParameters and DereferencedOperations and so on.

Anywhere that a type would have had either a reference or a component, the dereferenced variety will simply have the component. For example, PathItem has an array of parameters, each of which is Either<JSONReference<Parameter>, Parameter> whereas a DereferencedPathItem has an array of DereferencedParameters. The dereferenced variant of each type exposes all the same properties and you can get at the underlying OpenAPI type via an underlying{TypeName} property. This can make for a much more convenient way to traverse a document because you don't need to check for or look up references anywhere the OpenAPI Specification allows them.

You can take things a step further and resolve the document. Calling resolved() on a DereferencedDocument will produce a canonical form of an OpenAPI.Document. The ResolvedRoutes and ResolvedEndpoints that the ResolvedDocument exposes collect all relevant information from the whole document into themselves. For example, a ResolvedEndpoint knows what servers it can be used on, what path it is located at, and which parameters it supports (even if some of those parameters were defined in an OpenAPI.Operation and others were defined in the containing OpenAPI.PathItem).

If your end goal is to analyze the OpenAPI Document or generate something entirely new (like code) from it, the ResolvedDocument is by far more convenient to traverse and query than the original OpenAPI.Document. The downside is, there is not currently support for mutating the ResolvedDocument and then turning it back into an OpenAPI.Document to encode it.

let document: OpenAPI.Document = ...

let resolvedDocument = try document
    .locallyDereferenced()
    .resolved()

for endpoint in resolvedDocument.endpoints {
    // The description found on the PathItem containing the Operation defining this endpoint:
    let routeDescription = endpoint.routeDescription

    // The description found directly on the Operation defining this endpoint:
    let endpointDescription = endpoint.endpointDescription

    // The path, which in the OpenAPI.Document is the key of the dictionary containing
    // the PathItem under which the Operation for this endpoint lives:
    let path = endpoint.path

    // The method, which in the OpenAPI.Document is the way you access the Operation for
    // this endpoint on the PathItem (GET, PATCH, etc.):
    let httpMethod = endpoint.method

    // All parameters defined for the Operation _or_ the PathItem containing it:
    let parameters = endpoint.parameters

    // Per the specification, this is 
    // 1. the list of servers defined on the Operation if one is given.
    // 2. the list of servers defined on the PathItem if one is given _and_ 
    //	no list was found on the Operation.
    // 3. the list of servers defined on the Document if no list was found on
    //	the Operation _or_ the PathItem.
    let servers = endpoint.servers

    // and many more properties...
}

Curated Integrations

Following is a short list of integrations that might be immediately useful or just serve as examples of ways that OpenAPIKit can be used to harness the power of the OpenAPI specification.

If you have a library you would like to propose for this section, please create a pull request and explain a bit about your project.

Declarative OpenAPI Documents

The Swift Package Registry API Docs define the OpenAPI documentation for the Swift Package Registry standard using declarative Swift code and OpenAPIKit. This project also provides a useful example of producing a user-friendly ReDoc web interface to the OpenAPI documentation after encoding it as YAML.

Generating OpenAPI Documents

VaporOpenAPI / VaporOpenAPIExample provide an example of generating OpenAPI from a Vapor application's routes.

JSONAPI+OpenAPI is a library that generates OpenAPI schemas from JSON:API types. The library has some rudimentary and experimental support for going the other direction and generating Swift types that represent JSON:API resources described by OpenAPI documentation.

Semantic Diffing of OpenAPI Documents

OpenAPIDiff is a library and a CLI that implements semantic diffing; that is, rather than just comparing two OpenAPI documents line-by-line for textual differences, it parses the documents and describes the differences in the two OpenAPI ASTs.

Notes

This library does not currently support file reading at all muchless following $refs to other files and loading them in. You must read OpenAPI documentation into Data or String (depending on the decoder you want to use) and all references must be internal to the same file to be resolved.

This library is opinionated about a few defaults when you use the Swift types, however encoding and decoding stays true to the spec. Some key things to note:

  1. Within schemas, required is specified on the property rather than being specified on the parent object (encoding/decoding still follows the OpenAPI spec).
    • ex JSONSchema.object(properties: [ "val": .string(required: true)]) is an "object" type with a required "string" type property.
  2. Within schemas, required defaults to true on initialization (again, encoding/decoding still follows the OpenAPI spec).
    • ex. JSONSchema.string is a required "string" type.
    • ex. JSONSchema.string(required: false) is an optional "string" type.

See A note on dictionary ordering before deciding on an encoder/decoder to use with this library.

Contributing

Contributions to OpenAPIKit are welcome and appreciated! The project is mostly maintained by one person which means additional contributors have a huge impact on how much gets done how quickly.

Please see the Contribution Guidelines for a few brief notes on contributing the the project.

Security

The OpenAPIKit project takes code security seriously. As part of the Swift Server Workground incubation program, this project follows a shared set of standards around receiving, reporting, and reacting to security vulnerabilies.

Please see Security for information on how to report vulnerabilities to the OpenAPIKit project and what to expect after you do.

Please do not report security vulnerabilities via GitHub issues.

Specification Coverage & Type Reference

For a full list of OpenAPI Specification types annotated with whether OpenAPIKit supports them and relevant translations to OpenAPIKit types, see the Specification Coverage documentation. For detailed information on the OpenAPIKit types, see the full type documentation.

GitHub

link
Stars: 128
Last commit: 6 days ago

Ad: Job Offers

iOS Software Engineer @ Perry Street Software
Perry Street Software is Jack’d and SCRUFF. We are two of the world’s largest gay, bi, trans and queer social dating apps on iOS and Android. Our brands reach more than 20 million members worldwide so members can connect, meet and express themselves on a platform that prioritizes privacy and security. We invest heavily into SwiftUI and using Swift Packages to modularize the codebase.

Dependencies

Release Notes

We're seeing double now
8 weeks ago

NOTE: The following will refer to both OpenAPIKit v3.0.0 (this library's upcoming version) and also OpenAPI v3.0/v3.1 (two versions of the specification this library implements). Notice the Kit suffix is the easiest way to tell if I am talking about this library. It can be confusing to keep these straight, because this library follows semantic versioning (wherein v3.0.0 is a major version bump associated with breaking changes) and the specification itself does not follow semantic versioning (the move from v3.0 to v3.1 of the specification is associated with breaking changes) -- and, what's worse, it is merely coincidental that the specification is on v3.1 and the library is on v3.0.

Here's a table that might help:

OpenAPIKit OpenAPI v3.0 OpenAPI v3.1
v1.x
v2.x
v3.x

Release Notes

Version 3.0.0 of OpenAPIKit primarily focuses on supporting the new v3.1 of the OpenAPI specification. It will, however, also make some small breaking changes to support for the OpenAPI v3.0 specification where those changes fix bugs, drastically clean things up, or support really beneficial new features.

The release notes for this first alpha of OpenAPIKit 3.0.0 will not go into details on the differences between OpenAPIKit's implementation of v3.0 and v3.1 of the OpenAPI specification. It will, however, describe the biggest difference going into this major version of the OpenAPIKit library:

Two Modules. The two public modules are OpenAPIKit30 (support for v3.0 of the specification) and OpenAPIKit (support for v3.1 of the specification).

v3.0.0 of OpenAPIKit splits support for OpenAPI v3.0 and OpenAPI v3.1 into separate modules because there are incompatibilities between the two versions that are most easily handled by either picking which version of the specification to use or explicitly referring to the version being used if both are needed.

Migrating from OpenAPIKit v2.x

V3.0 of the specification

To use the OpenAPI v3.0 specification much like it has been supported in past versions of this library, change your imports as follows:

// before:
import OpenAPKit

// after:
import OpenAPIKit30

If you are using Swift Package Manager, also change your dependency from the string "OpenAPIKit" or the product .product(name: "OpenAPIKit", package: "OpenAPIKit") to .product(name: "OpenAPIKit30", package: "OpenAPIKit").

Beyond that, the only change you currently need to look out for is that JSONReferences inside JSONSchema has gained a context so that it can support optionality. This means switches that match may need to change as follows:

// before:
case .reference(let ref) ...

// after:
case .reference(let ref, let context) ...

The new context contains the required boolean. This boolean defaults to true, but can be made false if a reference should be optional (which means it will be left out of its parent's required array on serialization to JSON/YAML).

You can also access this context with the new referenceContext accessor on JSONSchema and you can now reliably test whether a reference schema is required with the JSONSchema required accessor (boolean for whether or not any particular schema is required).

v3.1 of the specification

As noted above, the release notes for this alpha won't go into details on the differences between OpenAPIKit's support for v3.0 and v3.1 of the OpenAPI specification. Future alpha & beta releases will detail some of these differences; for now, this release serves as a way to test some of those differences for practicality.

To use the new version, you use the OpenAPIKit module (as opposed to the OpenAPIKit30 module that supports v3.0 of the specification).

Thanks

Thanks goes to @siemensikkema for implementing support of required/optional reference schemas and @mihaelamj for implementing numerous changes required by the OpenAPI v3.1 specification.

Swiftpack is being maintained by Petr Pavlik | @ptrpavlik | @swiftpackco | API