OpenAPIKit

repository·main·Indexed 43 days ago

https://github.com/mattpolzin/openapikit

A Swift library for encoding, decoding, and validating OpenAPI 3.0.x, 3.1.x, and 3.2.x documents and their components. It provides type-safe Swift representations, support for JSON and YAML via Yams, and tools for internal and external dereferencing to resolve references into a canonical ResolvedDocument.

Tokens
15.1K
Snippets
52
Records
78
Agent score
54%

What's inside OpenAPIKit

  1. Overview of OpenAPIKit

    main

    OpenAPIKit is a Swift library designed to encode and decode OpenAPI Documents and their components. It provides type-safe Swift representations for the following OpenAPI specification versions:

    • OpenAPI 3.0.x
    • OpenAPI 3.1.x
    • OpenAPI 3.2.x

    The library follows semantic versioning. Use the following compatibility matrix to select the correct version for your project requirements:

    OpenAPIKitSwiftOpenAPI v3.0, v3.1OpenAPI v3.2
    v4.x5.8+✅
    v5.x5.10+✅✅
    v6.x6.1+✅✅
  2. Explore OpenAPIKit integrations and examples

    main

    OpenAPIKit is used by several tools and libraries in the Swift ecosystem. These can serve as inspiration or direct solutions for common tasks:

    • Code Generation: swift-openapi-generator uses OpenAPIKit to generate Swift code for interfacing with APIs.
    • Vapor Integration: VaporOpenAPI demonstrates how to generate OpenAPI documentation directly from a Vapor application's routes.
    • JSON:API Support: JSONAPI+OpenAPI generates OpenAPI schemas from JSON:API types.
    • Declarative Documentation: The Swift Package Registry API Docs use declarative Swift code and OpenAPIKit to define documentation, which can then be encoded to YAML for tools like ReDoc.
    • Semantic Diffing: OpenAPIDiff is a CLI and library that performs semantic diffing by parsing OpenAPI documents into ASTs rather than performing simple line-by-line text comparisons.
  3. OpenAPIKit Specification Coverage Overview

    main

    OpenAPIKit provides Swift representations for the OpenAPI Specification 3.2.x. This document tracks which OpenAPI objects and fields are supported by the library.

    Key mapping notes:

    • Some types have different names in OpenAPIKit than in the specification (noted in parentheses in the full documentation).
    • Most core objects like OpenAPI.Document, Info, Server, Components, PathItem, and Operation are fully supported.
    • JSONSchema provides support for most inherited JSON Schema keywords, including discriminator, readOnly, and writeOnly.
  4. Important: Validation constraints and types

    main

    Validation is performed by crawling the OpenAPI.Document structure during the encoding process. This leads to two important constraints:

    1. Validatable Protocol: You can only write validations against types that are marked with the Validatable protocol (most types in the OpenAPI namespace).
    2. Encoding Behavior: Because validation follows encoding, some types behave differently:
      • URLs: These are encoded as String. You cannot write a validation directly on URL; instead, validate the String or the parent type (e.g., OpenAPI.Server).
      • Dictionary Keys: Keys (like OpenAPI.Path or OpenAPI.Response.StatusCode) are not encoded directly as objects. To validate them, you must create validations against the types that contain these dictionaries.
      • Empty Collections: If a Map or Array is empty, it may be omitted from encoding entirely. In such cases, a validation targeting the Map/Array itself might not run. To check if a collection is empty, target the parent type (e.g., target Operation to check if its responses map is empty).
  5. Optimize compilation time using the External Loading Trait

    main

    The external loading capabilities (introduced in v4.0.0) are now implemented as a Trait. This trait is enabled by default.

    Because the Swift compiler can be slow when processing Swift Concurrency features, you can disable this trait to reduce compilation time by approximately 50%. There is no known runtime cost for leaving the trait enabled; the cost is strictly in compile time.

  6. Update OpenAPI Specification Version handling

    main

    The OpenAPI.Document.Version enum has been updated in both the OpenAPIKit and OpenAPIKit30 modules to support newer specifications and future-proof versioning:

    • OpenAPIKit module: Added v3_1_1 and a new case v3_1_x(x: Int) for future 3.1.x versions.
    • OpenAPIKit30 module: Added v3_0_4 and a new case v3_0_x(x: Int) for future 3.0.x versions.

    Action required: If you use exhaustive switch statements on these enum values, you must update them to handle the new cases.

  7. Parse future OpenAPI versions using version mapping

    main

    If you encounter an OpenAPI version newer than what OpenAPIKit natively supports, you can instruct the decoder to treat it as an older, supported version using the DocumentConfiguration.versionMapKey in the decoder's userInfo.

    Note: Once native support for that version is added to OpenAPIKit, the native handling will take precedence over your manual mapping.

    let userInfo = [
      DocumentConfiguration.versionMapKey: ["3.100.100": OpenAPI.Document.Version.v3_1_1]
    ]
    
    let decoder = ... // JSONDecoder() or YAMLDecoder()
    decoder.userInfo = userInfo
    
    let openAPIDoc = try decoder.decode(OpenAPI.Document.self, from: ...)
  8. Compose validations using lift, unwrap, and all

    main

    Complex validation logic can be built by composing smaller Validation objects:

    • lift(_:into:): Maps the current context to a new context using a KeyPath and runs a validation in that new context. Use this when the KeyPath is non-optional.
    • unwrap(_:into:): Similar to lift, but if the KeyPath encounters nil, it produces a ValidationError. This is useful for asserting that an optional field must exist.
    • lookup(_:): Used when a KeyPath ends in an Either<OpenAPI.Reference<T>, T>, allowing you to lift the context into validations targeting T (handling JSON References).
    • all(_:...): Combines multiple validations within the same context using logical AND.
    • Logical Operators: You can combine check or when arguments using && (AND) and || (OR).
    // Example: Composing a validation that checks if a specific response exists and contains JSON
    let contentValidation = Validation<OpenAPI.Content>(...)
    
    let successResponseBodyContainsNameAndId = Validation(
       check: unwrap(
           \OpenAPI.Response.Map[status: 201]?.responseValue,
           into: responseBodyContainsNameAndId,
           description: "201 status response value"
        )
    )
    
    // Using logical AND to combine unwrapped checks
    let postRequestAndResponsesAreValid = Validation(
        check: unwrap(
            \OpenAPI.PathItem[.post]?.requestBody?.requestValue,
            into: requestBodyContainsName
        )
        && unwrap(
            \OpenAPI.PathItem[.post]?.responses,
            into: successResponseBodyContainsNameAndId
        ),
        when: \OpenAPI.PathItem[.post] != nil
    )
  9. Understand OpenAPIKit's schema requirement defaults

    main

    OpenAPIKit is opinionated about how required properties are handled in Swift to improve developer ergonomics, even though encoding and decoding remain strictly compliant with the OpenAPI specification:

    1. Property-level specification: Within schemas, required is specified on the property itself rather than on the parent object. For example, JSONSchema.object(properties: [ "val": .string(required: true)]) defines an object with a required string property.
    2. Default to required: When initializing schema types, required defaults to true. To make a property optional, you must explicitly set it to false.

    Examples:

    • JSONSchema.string is a required string type.
    • JSONSchema.string(required: false) is an optional string type.
  10. How External Dereferencing works

    main

    External dereferencing loads files referenced via external URLs into the document's Components object and converts those external references into internal ones.

    To use this, you must implement the ExternalLoader protocol, providing:

    1. load<T>(_ url: URL): An async function to fetch and decode the data.
    2. componentKey<T>(type: T.Type, at url: URL): A function to determine the key under which the new component will be stored in the Components object.

    Once implemented, call document.externallyDereference(using: loader, depth: .full).

    struct ExampleLoader: ExternalLoader {
        static func load<T>(_ url: URL) async throws -> (T, [Message]) where T : Decodable {
            // Implementation to fetch and decode data
        }
    
        static func componentKey<T>(type: T.Type, at url: URL) throws -> OpenAPIKit.OpenAPI.ComponentKey {
            // Implementation to generate a key from the URL
        }
    }
  11. How Internal Dereferencing and Resolution work

    main

    Once a document is externally dereferenced (all external files are loaded into Components), you can perform internal dereferencing to remove all remaining Reference wrappers.

    1. locallyDereferenced(): Converts an OpenAPI.Document into a DereferencedDocument. In this state, types like PathItem or Operation no longer contain Either<Reference<T>, T>, but instead contain the direct type T (e.g., DereferencedParameter). This makes traversing the document much easier as you don't need to handle references.
    2. resolved(): Called on a DereferencedDocument to produce a ResolvedDocument. This is a canonical form where all information (like servers, paths, and parameters) is collected into single, easy-to-query objects like ResolvedEndpoint. This is ideal for code generation or analysis, though ResolvedDocument is not currently mutable.
    let document: OpenAPI.Document = ...
    
    let resolvedDocument = try document
        .locallyDereferenced()
        .resolved()
    
    for endpoint in resolvedDocument.endpoints {
        let path = endpoint.path
        let method = endpoint.method
        let parameters = endpoint.parameters
        let servers = endpoint.servers
    }
  12. Understand `JSONSchema` context optionality and `nullable` behavior

    main

    To support schema fragments, several properties in schema contexts are now optional.

    Important Note on nullable: In v2, the nullable property can now be true, false, or nil. While nil is interpreted as false during encoding/decoding, they are now distinct states. Avoid logic that assumes a missing nullable property is strictly equivalent to nullable: false in code.