Swiftpack.co - Package - mattpolzin/OpenAPIKit

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, 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, 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 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.

OpenAPI Document structure

The types used by this library largely mirror the object definitions found in the OpenAPI specification version 3.0.2. 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 properties are given as arguments to static constructors. By default, schemas are non-nullable, required, and generic.

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 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.

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.

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: 95

Used By

Total: 0

Releases

Tell Me More - 2020-10-01 00:48:36

A minor patch to the message output when validation of .schemaComponentsAreDefined fails.

2.0.0 - Much Love for Schemas - 2020-09-28 01:36:29

Features & Additions

New schema validation

New optional schema validation (not included in validation by default): .schemaComponentsAreDefined. Validates that all schemas are at least minimally defined (i.e. none of the JSON Schema definitions are just an empty object).

LocallyDereferenceable

Most OpenAPI types are LocallyDereferenceable now, which means they offer a dereferenced(in:) method that takes an OpenAPI.Components and results in a new type that is guaranteed to not contain any JSONReferences. This is the same behavior exposed on OpenAPI.Document via its locallyDereferenced() method.

Schema simplification

DereferencedJSONSchema has gained the simplified() method which will attempt to simplify compound schemas. The same is exposed via JSONSchema's simplified(given:) method which takes OpenAPI.Components.

Simplification is in its early stages right now and does not support all possible schemas but it works well on all(of:) schemas already.

Bug Fixes

  • OpenAPI.Server's url used to not be able to handle server URLs with variables in them. This has been fixed in the new urlTemplate property (see breaking changes and the migration guide for more information).
  • JSONSchema can express a number of valid schemas it previously could not -- all of its cases support the core set of properties (see breaking changes and the migratio guide for more information).
  • OpenAPI.Content's schema property has become optional, as indicated by the OpenAPI Specification (under Media Type Object).

Breaking Changes

The following list enumerates the breaking changes but the migration guide serves as a better resource for identifying the changes needed in your codebase (if any).

  • OpenAPI.Server loses its url property in favor of a more correct urlTemplate property that can in turn be asked for a Foundation URL (but it will only produce one if the URL in question has no variables in it).
  • OpenAPI.Content's schema property has become optional to support valid OpenAPI documentation where the Media Type Object has no schema.
  • JSONSchemaFragment has been removed -- you can now use JSONSchema to represent all of the same schema fragments. Relatedly, JSONSchema's undefined case has been renamed fragment and it has gained a full CoreContext.
  • DereferencedJSONSchema's underlyingJSONSchema property has been renamed to jsonSchema.
  • The JSONSchema generalContext property and Context type have been renamed to coreContext and CoreContext. DereferencedJSONSchema has also renamed its Context to CoreContext.
  • JSONSchema's all(of:), any(of:), and one(of:) cases had their discriminator associated value replaced with a full CoreContext (which in turn has a discriminator).
  • JSONSchema's not case gained a CoreContext.
  • Some additional properties of the JSONSchema.CoreContext (and its other contexts) have become optional to support a broader range of valid schemas. In practice, this is mostly non-breaking because the accessors remain non-optional even where the underlying type has changed but the initializers now take optional input to support the new expressivity.
  • JSONSchema's dereferencedSchemaObject() and dereferencedSchemaObject(resolvingIn:) renamed to dereferenced() and dereferenced(in:).
  • OpenAPI.Components dereferencing and lookup methods have been renamed to better express the differences between "looking a resource up" and "dereferencing a resource." See the migration guide for more information.

Improvements in-depth

Dereferencing

In OpenAPIKit v1, the OpenAPI.Components type offered the methods dereference(_:) and forceDereference(_:) to perform lookup of components by their references. These were overloaded to allow looking up Either types representing either a reference to a component or the component itself.

In OpenAPIKit v1.4, true dereferencing was introduced. True dereferencing does not just turn a reference into the value it refers to, it removes references iteratively for all properties of the given value. That made the use of the word "dereference" in the OpenAPI.Components type's methods misleading -- these methods "looked up" values but did not "dereference" them.

OpenAPIKit v2 fixes this confusing naming by supporting component lookup via subscript (non-throwing) and lookup(_:) (throwing) methods and not offering any methods that truly dereference types. At the same time, OpenAPIKit v2 adds the dereferenced(in:) method to most OpenAPI types. This new method takes an OpenAPI.Components value and returns a fully dereferenced version of self. The dereferenced(in:) method offers the same iterative dereferencing behavior exposed by the OpenAPI.Document locallyDereferenced() method that was added in OpenAPIKit v1.4.

Simplifying all(of:) schemas

The JSONSchema all(of:) case is unique amongst the combination cases because all of the schema fragments under it can often effectively be merged into a new schema. This is what "simplifying" that schema means in OpenAPIKit. The result of simplifying an all(of:) schema is a DereferencedJSONSchema.

Simplification is performed by JSONSchema's simplified(given:) method (which takes an OpenAPI.Components value) or the DereferencedJSONSchema simplified() method.

let schemaData = """
{
    "allOf": [
        {
            "type": "object",
            "description": "A person",
            "required": [ "name" ],
            "properties": {
                "name": { "type": "string" }
            }
        },
        {
            "type": "object",
            "properties": {
                "favoriteColor": {
                    "type": "string",
                    "enum": [ "red", "green", "blue" ]
                }
            }
        }
    ]
}
""".data(using: .utf8)!
let personSchema = try JSONDecoder().decode(JSONSchema .self, from: schemaData)
    .simplified(given: .noComponents)

// results in simplified schema equivalent to:
let personSchemaInCode = JSONSchema.object(
    description: "A person",
    properties: [
        "name": .string,
        "favoriteColor": try JSONSchema.string(required: false, allowedValues: "red", "green", "blue")
    ]
)

Default schema optionality

In OpenAPIKit v1, schemas created in-code defaulted to required: true. While decoding, however, they would default to required: false and then if a JSON Schema .object had a "required" array containing a certain property, that property's required boolean would get flipped to true. This is somewhat intuitive at face value, but it has the unintuitive side effect of all root schema components (i.e. a JSON Schema that does not live within another .object) having required: false because there is no parent "required" array to cause OpenAPIKit to flip its required boolean.

Again, this has no effect on the accuracy of encoding/decoding because the required boolean of a JSONSchema value is only encoded as part of a parent schema's "required" array.

OpenAPIKit v2 swaps this decoding default to required: true and instead flips the boolean to false for all properties not found in a parent object's "required" array. This approach has the same effect upon encoding/decoding except for root schemas having required: true which both aligns better with the default in-code required boolean and also makes more intuitive sense.

let schemaData = """
{ 
    "type": "object",
    "required": [
        "test"
    ],
    "properties": {
        "test": {
          "type": "string"
        }
    }
}
""".data(using: .utf8)!

//
// OpenAPIKit v1
//
let schema1 = try JSONDecoder().decode(JSONSchema.self, from: schemaData)

// results in:
let schema1InCode = JSONSchema.object(
    required: false, // <-- note that this is unintuitive even though it has no effect on the correctness of the schema when encoded.
    properties: [
        "test": .string(required: true) // <-- note that this `required: true` could be omitted, it is the default for in-code construction.
    ]
)

//
// OpenAPIKit v2
//
let schema2 = try JSONDecoder().decode(JSONSchema.self, from: schemaData)

// results in:
let schema2InCode = JSONSchema.object(
    required: true, // <-- note that this `required: true` could be omitted, it is the default for in-code construction.
    properties: [
        "test": .string(required: true) // <-- note that this `required: true` could be omitted, it is the default for in-code construction.
    ]
)

Version 2.0.0 Release Candidate 1 - 2020-09-22 03:15:51

There are no substantial changes since the last beta and this release candidate. Test coverage has improved, a few gaps in the API have been closed, and the migration guide has been completed.

The v2 release notes (on the upcoming release) will summarize changes and the migration guide will be a resource to help quickly identify what breaking changes require attention.

Version 2.0.0 Beta 2 - 2020-09-12 21:53:05

Adds a new optional validation: All schemas are at least minimally defined (i.e. none of the JSON Schema definitions are just an empty object).

let document: OpenAPI.Document = ...
let validator = Validator.blank.validating(.schemaComponentsAreDefined)
try document.validate(using: validator)

Version 2.0.0 Beta 1 - 2020-09-12 19:27:53

The first beta signals a relatively stable API as far as major breaking changes go.

A Migration guide has been added and will be filled out over the coming weeks, although it is incomplete for now and developers should continue to use the release notes for the 4 alpha releases to determine what changes need to be made to begin working with OpenAPIKit v2.

The only breaking-ish change between the last Alpha and the first Beta release is the Yams dependency (used only for test targets in OpenAPIKit) was bumped by a major version.

Version 2.0.0 Alpha 4 - 2020-09-06 02:00:01

Reintroduced support for all(of:) in DereferencedJSONSchema. Whereas dereferencing a JSONSchema performed a step aimed at simplifying away (and therefore removing) all(of:), alpha 4 moves that process into a separate method that can be called on-demand. A few bugs related to simplification have also been fixed.

⚠️ Breaking Changes ⚠️

  • The behavior previously attached to dereferencing a JSONSchema has been moved to JSONSchema's simplified(given:) and DereferencedJSONSchema's simplified() methods.
  • DereferencedJSONSchema now has an all(of:) case.

Version 2.0.0 Alpha 3 - 2020-08-29 19:03:10

Closes https://github.com/mattpolzin/OpenAPIKit/issues/143.

The all(of:), any(of:), one(of:), and not JSON Schema cases in OpenAPIKit used to only support an optional discriminator as metadata directly on the schema component containing the compound property. Now they all support the full CoreContext (with any format) so that title, description, etc. can be specified on them. This version also closes a gap in representability of required/nullable/optional compound schemas.

This kind of thing was not previously representable in OpenAPIKit:

{
  "title": "a compound thing",
  "oneOf": [
    ...
  ]
}

⚠️ Breaking Changes ⚠️

This is a breaking change for the aforementioned cases on JSONSchema.

If you previously matched on .not(let schema), you now must match on .not(let schema, core: let coreContext).

If you previously matched on .any(of: let schemas, discriminator: let discriminator) (or the similar patterns for all or one), you now must match on .any(of: let schemas, core: let coreContext); you will find the discriminator at coreContext.discriminator.

If you previously created a schema with .any(of: [...], discriminator: nil) (or the similar patterns for all or one), you can now just create the default empty CoreContext: .any(of: [...], core: .init()).

Version 2.0.0 Alpha 2 - 2020-08-23 15:27:39

This release unifies JSONSchema and JSONSchemaFragment. The two types were growing closer and closer together and realistically the differences were weaknesses in both types.

The big difference here is that the JSONSchema undefined case has changed to a fragment case. Whereas undefined schemas could only have descriptions, the fragment case can store all of the various information needed by an unspecialized schema (i.e. one that has no properties that could be used to identify its type).

The JSONSchemaFragment type has been removed entirely.

Version 2.0.0 Alpha 1 - 2020-08-16 01:16:21

Version 2 of OpenAPIKit adds a few features, fixes some bugs requiring breaking changes, lays groundwork for future work, and renames some types that had started to feel inconsistent with other parts of OpenAPIKit.

As with any alpha release, do not expect the changes in this release to represent a finished stable API. That said, most of the breaking changes intended for this major version are introduced with this first alpha.

Additions

  • Most types are now LocallyDereferenceable which means they offer a dereferenced(in:) method that takes OpenAPI.Components as its argument and performs a recursive deference operation on self -- the same behavior exposed on OpenAPI.Document via its locallyDereferenced() method.
  • Arrays of schema fragments ([JSONSchemaFragment]) like those found under the JSONSchema all(of:) case can be resolved into a JSONSchema representation using the array extension resolved(against:).
  • A new URLTemplate type supports templated URLs like Server Objects use. It will be the basis for future additions like variable replacement and template URL resolution.

Removals

  • OpenAPI.Components dereference(_:) was removed in favor of the subscript with the same signature.
  • The various Dereferenced versions of types lose their public initializer. These types should only be constructed using the dereferenced(in:) method of the originating OpenAPI type. For example, to get a DereferencedPathItem, you use the OpenAPI.PathItem dereferenced(in:) method.
  • DereferencedJSONSchema no longer has an all(of:) case. Instead, part of dereferencing a schema is now the process of resolving an all(of:) schema. More on this below.

Changes

  • OpenAPI.Components forceDereference(_:) was renamed to lookup(_:). More on this below.
  • DereferencedJSONSchema underlyingJSONSchema property was renamed to jsonSchema to avoid the misconception that this property stores the JSONSchema that produced the given DereferencedJSONSchema. In reality, this property will build the JSONSchema representing the given DereferencedJSONSchema.
  • JSONSchema.Context, DereferencedJSONSchema.Context, and JSONSchemaFragment.GeneralContext have been renamed to JSONSchema.CoreContext, DereferencedJSONSchema.CoreContext, and JSONSchemaFragment.CoreContext to align and settle on a better name for "the context shared by most cases." With that, the JSONSchema generalContext property has been renamed to coreContext.
  • The default optionality of JSONSchema values when decoded has been swapped from optional to required. This has no effect on encoding/decoding (perhaps counter-intuitively) because in any JSON Schema structure, the optionality of a property on an object is actually determined by the required array on that object and this fact remains true despite the default changing. More on this below.
  • Instead of allowing Validation to use any type as a subject, the Validatable protocol has been introduced with all types that will work for validation contexts getting conformance. This allows the type checker to steer you away from writing validations that will never be run. NOTE that URL is encoded as a string for compatibility with the broadest range of encoders and therefore URL is not Validatable. You can still use String as a validation subject or write validations in terms of the OpenAPIKit types that contain URL properties. See the full Validation Documentation for more.
  • The OpenAPI.Server type's url property was renamed urlTemplate and its type changed from a Foundation URL to a URLTemplate. This change fixes the bug that OpenAPIKit did not used to be able to decode or represent OpenAPI Template URLs (those with variables in them). You can get a Foundation URL from a URLTemplate via the url property but only urls without any variable placeholders can be turned into Foundation URLs directly.
  • The OpenAPI.Content type's schema property has become optional. Requiring the schema property was not compliant with the OpenAPI specification (see the Media Item Object definition).

Details

Dereferencing

In OpenAPIKit v1, the OpenAPI.Components type offered the methods dereference(_:) and forceDereference(_:) to perform lookup of components by their references. These were overloaded to allow looking up Either types representing either a reference to a component or the component itself.

In OpenAPIKit v1.4, true dereferencing was introduced. True dereferencing does not just turn a reference into the value it refers to, it removes references recursively for all properties of the given value. That made the use of the word dereference in the OpenAPI.Components type's methods misleading -- this methods "looked up" values but did not "dereference" them.

OpenAPIKit v2 fixes this confusing naming by supporting component lookup via subscript (non-throwing) and lookup(_:) (throwing) methods and not offering any methods that truly dereference types. At the same time, OpenAPIKit v2 adds the dereferenced(in:) method to most OpenAPI types. This new method takes an OpenAPI.Components value and returns a fully dereferenced version of self. The dereferenced(in:) method offers the same recursive dereferencing behavior exposed by the OpenAPI.Document locallyDereferenced() method that was added in OpenAPIKit v1.4.

Resolving all(of:) schemas

The JSONSchema all(of:) case is unique amongst the combination cases because all of the schema fragments under it can effectively be merged into a new schema. This is what "resolving" that schema means in OpenAPIKit. The result of resolving an all(of:) schema is a DereferencedJSONSchema.

For the most part, resolution will just happen as part of dereferencing or resolving anything that contains a JSONSchema that happens to have all(of:) cases but you do also have the ability to take an array of JSONSchemaFragment and resolve it directly with the array extension resolved(against:) which takes an OpenAPI.Components to resolve the array of schema fragments against.

let schemaData = """
{
    "allOf": [
        {
            "type": "object",
            "description": "A person",
            "required": [ "name" ],
            "properties": {
                "name": { "type": "string" }
            }
        },
        {
            "type": "object",
            "properties": {
                "favoriteColor": {
                    "type": "string",
                    "enum": [ "red", "green", "blue" ]
                }
            }
        }
    ]
}
""".data(using: .utf8)!
let personSchema = try JSONDecoder().decode(JSONSchema .self, from: schemaData)
    .dereferenced(in: .noComponents)

// results in:
let personSchemaInCode = JSONSchema.object(
    description: "A person",
    properties: [
        "name": .string,
        "favoriteColor": try JSONSchema.string(required: false, allowedValues: "red", "green", "blue")
    ]
)

Default schema optionality

In OpenAPIKit v1, schemas created in-code defaulted to required:true. While decoding, however, they would default to required:false and then if a JSON Schema .object had a required array containing a certain property, that property's required boolean would get flipped to true. This is somewhat intuitive at face value, but it has the unintuitive side effect of all root schema nodes (i.e. a JSON Schema that does not live within another .object) will have required: false because there was no parent required array to cause OpenAPIKit to flip its required boolean.

Again, this has no effect on the accuracy of encoding/decoding because the required boolean of a JSONSchema is only encoded as part of a parent schema's required array.

OpenAPIKit v2 swaps this decoding default to required: true and instead flips the boolean to false for all properties not found in a parent object's required array. This approach has the same effect upon encoding/decoding except for root schemas having required: true which both aligns better with the default in-code required boolean and also makes more intuitive sense.

let schemaData = """
{ 
    "type": "object",
    "required": [
        "test"
    ],
    "properties": {
        "test": {
          "type": "string"
        }
    }
}
""".data(using: .utf8)!

//
// OpenAPIKit v1
//
let schema1 = try JSONDecoder().decode(JSONSchema.self, from: schemaData)

// results in:
let schema1InCode = JSONSchema.object(
    required: false, // <-- note that this is unintuitive even though it has no effect on the correctness of the schema when encoded.
    properties: [
        "test": .string(required: true) // <-- note that this `required: true` could be omitted, it is the default for in-code construction.
    ]
)

//
// OpenAPIKit v2
//
let schema2 = try JSONDecoder().decode(JSONSchema.self, from: schemaData)

// results in:
let schema2InCode = JSONSchema.object(
    required: true, // <-- note that this `required: true` could be omitted, it is the default for in-code construction.
    properties: [
        "test": .string(required: true) // <-- note that this `required: true` could be omitted, it is the default for in-code construction.
    ]
)

Enum-be-gone - 2020-08-15 16:38:28

The OpenAPI.Server.Variable type incorrectly required that the enum property be defined (on decoding). This has now been fixed.

Statusfied - 2020-08-03 15:22:13

Adds a subscript overload to OrderedDictionary when the Key is an OpenAPI.Respose.StatusCode so that elements can be accessed using the status keyword.

Because OrderedDictionary is both a dictionary and a collection, it has always offered subscript access via both Key and Index (Int). OpenAPI.Response.StatusCode is ExpressibleByIntegerLiteral so accessing ordered dictionaries of OpenAPI.Response from the OpenAPI.Operation responses property by Int is ambiguous:

operation.responses[200] // <- is this `StatusCode` 200 or the 200th index of the collection?

Now, code accessing the dictionary with a status key can be written:

operation.responses[status: 200]

Of course, you can still use any other method of constructing a status code as well:

operation.responses[.status(code: 200)] // <- equivalent to above
operation.responses[.range(.success)]   // <- means the OpenAPI status code string "2XX"
operation.responses[.default]

Operation: Callback - 2020-08-01 01:25:16

Don't fail to decode documents that have callbacks properties in their Operation Objects (although callbacks are not yet supported by OpenAPIKit yet).

Heading the Right Direction - 2020-07-29 01:17:52

Improved error reporting around decoding invalid specification extensions and allowed some Header Object properties that were previously disallowed by accident when specification extension support was added to that type.

Unsupported Support - 2020-07-28 00:32:41

This fix makes types that are not yet supported by OpenAPIKit (see the Specification Coverage) ignored silently instead of letting them cause decoding to fail.

The idea is, it's better to still be able to parse documentation even if not all of it has OpenAPIKit representation yet. OpenAPIKit is really close to supporting all OpenAPI types but not quite there yet. At least now the existence of those types in a document won't render the document invalid in the eyes of OpenAPIKit.

Fragmented References - 2020-07-25 23:22:56

Fixes https://github.com/mattpolzin/OpenAPIKit/issues/98 wherein an allOf Schema Object with a Reference Object would not parse and could not be represented in OpenAPIKit.

⚠️ Breaking Change ⚠️ The fix involved adding the .reference case to the JSONSchemaFragment type. Although the surface area here is small, it is possible that this breaks code -- specifically, code that switches over the JSONSchemaFragment enum.

Required Stability - 2020-07-15 01:13:26

JSONSchema objects' requiredProperties/optionalProperties are now sorted so that their ordering is stable. These properties are dynamically created from the properties dictionary and therefore the order has never been defined before.

Even now, the order is not publicly guaranteed, but now it will at least be stable which can help with diffing.

U.R.Love - 2020-07-13 05:44:24

URLs now encode as strings and decode from strings no matter what encoder/decoder you use. This is the treatment already given to URLs by popular encoders and decoders like Foundation's JSON options and Yams's YAML options but now OpenAPIKit takes the liberty of passing URLs to the encoder (and requesting them from the decoder) as strings to make sure that they always get this intuitive treatment.

True Forms - 2020-07-03 04:02:24

This is a big release that closes two longstanding gaps: An inability to dereference an OpenAPI Document and the lack of any canonical representation of the components of the API described by an OpenAPI Document.

Dereferencing

The OpenAPI specification supports the use of JSON References in a number of places throughout the Document. These references allow authors to reuse one component in multiple places or even just make the route definition section of the document more concise by storing some components in the Components Object.

When traversing an OpenAPI document, these references are often more of an annoyance than anything else; Every time you reach a part of the document where a reference is allowed, you must check whether you are working with a reference or not or maybe even reach out to the Components Object and look a reference up.

This release introduces the OpenAPI.Document locallyDereferenced() method. This method traverses the whole document resolving all references to components found in the Components Object. The result is a document that has replaced many types with dereferenced variants -- anywhere you would have seen Either<JSONReference<Thing>, Thing> you will now just see Thing (or a DereferencedThing, anyway). The dereferenced variants will always expose the properties of the original OpenAPI type.

Before:

let document: OpenAPI.Document = ...

let anOperation: OpenAPI.Operation = document
    .paths["/hello/world"]!
    .get!

let parametersOrReferences = anOperation.parameters

// print the name of all parameters that happen to be inlined:
for parameter in parametersOrReferences.compactMap({ $0.parameterValue }) {
    print(parameter.name)
}

// resolve parameters in order to print the name of them all
for parameter in parametersOrReferences.compactMap(document.components.dereference) {
    print(parameter.name)
}

Now:

let document: OpenAPI.Document = ...

let anOperation = try document
    .locallyDereferenced()  // new
    .paths["/hello/world"]!
    .get!

let parameters = anOperation.parameters

// loop over all parameters and print their names
for parameter in parameters {
    print(parameter.name)
}

Resolving (to canonical representations)

The OpenAPI Specification leaves numerous opportunities (including JSON References) for authors to write the same documentation in different ways. Sometimes when analyzing an OpenAPI Document or using it to produce something new (a la code generation) you really just need to know what the API looks like, not how the author of the documentation chose to structure the OpenAPI document.

This release introduces the resolved() method on the DereferencedDocument (the result of the locallyDereferenced() method on an OpenAPI.Document). A ResolvedDocument collects information from all over the OpenAPI.Document to form canonical definitions of the routes and endpoints found within. In a resolved document, you work with ResolvedRoute (the canonical counterpart to the OpenAPI.PathItem) and ResolvedEndpoint (the canonical counterpart to the OpenAPI.Operation).

To show the power of a resolved type, let's look at the hypothetical need to look at all parameters for a particular endpoint. To achieve this without resolved types, we need to collect parameters on the Path Item Object and combine them with those on the Operation Object. Even worse, to really get this right we would need to let the parameters of the Operation override those of the Path Item if the parameter names & locations were the same.

Before:

let document: OpenAPI.Document = ...

let aPathItem = try document
    .locallyDereferenced()
    .paths["/hello/world"]!

let anOperation = aPathItem.get!

// going to oversimplify here and not worry
// about collisions between the Path Item and
// Operation parameters.
let parameters = aPathItem.parameters + anOperation.parameters

After:

let document: OpenAPI.Document = ...

let anEndpoint = try document
    .locallyDereferenced()
    .resolved()  // new
    .routesByPath["/hello/world"]!  // new
    .get!

// no oversimplification here, this is going to get
// us the totally correct comprehensive list of
// parameters for the endpoint.
let parameters = anEndpoint.parameters

Schema context accessors and all servers in Document - 2020-06-19 01:09:34

Additions

  • Adds allServers on OpenAPI.Document to retrieve all servers that are referenced anywhere in the document (whether in the root server array, the servers on a Path Item, or the servers on an Operation.
  • Adds accessors that retrieve contexts on JSONSchemas, providing an alternative to destructuring when retrieving an optional context is more convenient.

Bug Fixes

  • Breaking: Fixes return type on Components.forceDereference() to be non-optional. It was made optional when forceDereference() was introduced in v1.2.0 by accident even though it is not possible for that function to produce an optional value. I decided this was an isolated enough breakage to something introduced very recently and therefore am not waiting for a major version to fix the bug.

OpenAPI Document Extensible Validation - 2020-06-14 01:10:12

Additions

  • Add isInternal and isExternal to JSONReference.
  • Add forceDereference() methods to OpenAPI.Components to give a throwing alternative to the existing dereference() methods.
  • Add validation, as described below.

Validation

It has always been a goal of OpenAPIKit to lean into Swift's type system and make it impossible to represent invalid OpenAPI documents as much as possible. However, there are some things Swift's type system still cannot guarantee. For those things, now there is explicit validation.

Given an OpenAPI.Document, you can call validate() to check the following things (language pulled from the OpenAPI Specification):

  • The Responses Object MUST contain at least one response code, and it SHOULD be the response for a successful operation call.
  • Each tag name in the list [on the root Document Object] MUST be unique.
  • The list [of parameters on a Path Item] MUST NOT include duplicated parameters.
  • The list [of parameters on an Operation] MUST NOT include duplicated parameters.
  • [Operation Ids] MUST be unique among all operations described in the API.

You can use the validation system to exercise additional control over what a "valid" document looks in your particular use-case. For more information on adding your own validation checks, see the new Validation Documentation.

Expanded Vendor Extension Support - 2020-05-26 20:28:50

Expand vendor extension (i.e. "specification extension") support to:

  • OpenAPI.Request

  • OpenAPI.Response

  • OpenAPI.SecurityScheme

  • OpenAPI.Server.Variable

  • Add routes accessor on OpenAPI.Document to retrieve an array of pairings of Path and PathItem.

Bump Yams version. - 2020-05-14 00:15:23

⚠️ Technically this release performs a major version bump of the Yams library.

Looking over the Yams v3 release, I am inclined to say that v3 of Yams is fully source-compatible with v2 of Yams. There is a noted breaking change of now requiring Swift 4.1+ but OpenAPIKit only supports Swift 5.1+ to begin with.

The upshot of this major version bump is that even though OpenAPIKit only uses Yams for test targets, now downstream packages will not experience dependency conflicts when using OpenAPIKit and trying to use the latest versions of Yams (which include some important bug fixes).

First stable release - 2020-05-13 01:40:44

Closes https://github.com/mattpolzin/OpenAPIKit/issues/38. Closes https://github.com/mattpolzin/OpenAPIKit/issues/61. Closes https://github.com/mattpolzin/OpenAPIKit/issues/64.

As the version number implies, this signals my intention to stop making breaking changes to OpenAPIKit until a v2 release. If you have been playing along so far, I appreciate your patience with the frequent breaking changes throughout the pre-release period.

Additions

  1. OpenAPISchemaType conformances for URL and UUID.
  2. OpenAPI.SecurityScheme.SecurityType.name exposes a String representable enum value of the name of the security scheme type (oauth2, openIdConnect, http, apiKey).
  3. OpenAPI.Operation.ResponseOutcome and the .responseOutcomes property on Operation expose an array of response outcomes (structs isomorphic to the key/value pairs of the Response.Map for the Operation).
  4. Response.StatusCode.isSuccess to determine if a status code is in the 200 range (including the "2xx" syntax).
  5. JSONSchema.ObjectContext.optionalProperties to pair with the existing spec-mandated JSONSchema.ObjectContext.requiredProperties.
  6. Support for discriminators in JSONSchema.

⚠️ Breaking Changes ⚠️

  1. Moves OpenAPI.PathItem.Operation to OpenAPI.Operation and OpenAPI.PathItem.Parameter to OpenAPI.Parameter.

Nesting these types was both non-essential and also often annoyingly verbose at the call site.

  1. Renames OpenAPI.Parameter.Schema to OpenAPI.Parameter.SchemaContext.

Parameter.Schema is not a schema but rather a struct containing a schema, among other things. SchemaContext is a better name for this structure.

  1. Renames OpenAPI.HttpVerb to OpenAPI.HttpMethod and OpenAPI.PathItem.Endpoint.verb to OpenAPI.PathItem.Endpoint.method.

"Method" is pretty ubiquitous and even the word the OpenAPI Specification uses. I must have had a bit of writers block when I named it HttpVerb originally.

  1. Renames OpenAPI.Parameter.schemaOrContent.schemaValue to OpenAPI.Parameter.schemaOrContent.schemaContextValue.

This goes along with the name change from Parameter.Schema to Parameter.SchemaContext. The callsite makes more sense with this naming and the added .schemaValue that now directly refers to the underlying JSONSchema is more useful.

  1. Changes PathItem.Endpoint from a tuple to a struct.

  2. JSONSchema gains additional associated values for all(of:), any(of:), and one(of:) to support discriminators.

Fix error reporting on Path decoding - 2020-05-03 05:13:07

Fixes errors on Path where nested Either decoding errors are involved.

Adding specification extension support - 2020-04-28 03:06:08

Added specification extensions for:

  • Document
  • Document.Info
  • PathItem
  • Components
  • Operation
  • Server
  • Contact
  • License
  • Parameter

Added Parameter.location to expose a raw representable enum that just represents location (query, header, path, cookie).

⚠️ Breaking Changes ⚠️

  • Rename Parameter.Location to Parameter.Context.
  • Rename Parameter.parameterLocation to Parameter.context.

Fixes a bug with decoding whole number floats as integers - 2020-04-23 00:33:33

Decoding was inadvertently strict w/r/t whole number floats (like "1.0") being parsable as the Int type, specifically when using certain decoders.

Fixes https://github.com/mattpolzin/OpenAPIKit/issues/56.

Better JsonSchema allOf support - 2020-04-22 02:07:31

  1. Added .other case to OpenAPI.ContentType.
  2. Added PathItem.endpoints to get an array of all endpoints defined for the particular path.
  3. Added validation that security schemes referenced from PathItem.Operations can be found in the Components.
  4. Added JSONSchemaFragment to allow JSONSchema.all(of:) case to represent less than whole JSONSchemas.
  5. Fixed bug where JSONSchema would not parse anything without an explicit type specified -- upon further reading of the JSON Schema specification, it is fine to omit the type property. Now JSONSchema is happy as long as it has some way to infer the type.

⚠️ Breaking Changes ⚠️

  1. Renamed the wildcard ContentTypes to be "any" instead of "all":
  • all -> any
  • applicationAll -> anyApplication
  • audioAll -> anyAudio
  • imageAll -> anyImage
  • textAll -> anyText
  • videoAll -> anyVideo
  1. JSONSchema.all(of:) used to store an array of JSONSchema whereas now it stores an array of JSONSchemaFragment.

Better JSONSchema examples and minor OrderedDictionary error improvements - 2020-03-30 02:20:59

  • OrderedDictionary produces better error output when it fails to decode OpenAPI.ComponentKey (the keys of components in the Components Object).
  • JSONSchema is aligned with OpenAPI.Example in using AnyCodable for examples.

⚠️ Breaking Changes ⚠️ JSONSchema example properties switched from String to AnyCodable. This aligns with the examples of the Example Object (OpenAPI.Example). AnyCodable is preferable here because it will get encoded as a JSON structure instead of a String and that will be presented more favorably in UIs such as ReDoc. Any Encodable thing can still be turned into a String, wrapped in AnyCodable, and passed in to produce the result that you used to get, the work to do so is just a bit more exposed.

Change `openAPISchema()` function to property. - 2020-03-20 23:35:11

⚠️ Breaking Changes ⚠️ The throwing openAPISchema() function has been changed to the openAPISchema property (which is, naturally, non-throwing) because this function never needed to throw anyway in the end. It has been quite a few versions since any code relied on a throwing variant of it.

Bring dependencies into project - 2020-03-20 22:49:50

⚠️ Breaking Changes ⚠️ Brings Poly, OrderedDictionary, and AnyCodable dependencies into project, additionally making small tweaks and (in the case of Either) substantial additions to the available interface.

Either

Support now offered by the in-library Either type instead of the Poly2 type.

Adds OpenAPI.Component.dereference() function and named accessors for all existing types OpenAPIKit wraps in Either.

OrderedDictionary and AnyCodable

No changes to interface except for only adding the AnyCodable type (no need for the AnyEncodable or AnyDecodable types).