Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
a7286c6
update minimum swift version to 6.2, add migration guide, update README
mattpolzin May 14, 2026
506c075
feat: parse and preserve $dynamicRef (JSON Schema 2020-12)
aqeelat Jul 7, 2026
f44d178
docs: refine v7 migration guide and mark external-deref TODO per review
aqeelat Jul 9, 2026
ed64a6d
Merge pull request #501 from aqeelat/feature/359/dynamic-ref
mattpolzin Jul 9, 2026
26b74d9
feat: resolve $dynamicRef against the dynamic scope during local dere…
aqeelat Jul 11, 2026
c00e1a6
refactor(deref): share cycle-check + lookup via continuation helper
aqeelat Jul 18, 2026
6034d4e
Merge pull request #504 from aqeelat/dynamic-scope-resolution
mattpolzin Jul 19, 2026
e32198e
Merge branch 'main' into release/7_0
mattpolzin Jul 19, 2026
2902d80
Merge branch 'main' into release/7_0
mattpolzin Jul 29, 2026
1a2cc0f
feat: externally dereference $dynamicRef
aqeelat Jul 10, 2026
31a77ef
Merge pull request #503 from aqeelat/external-deref
mattpolzin Jul 29, 2026
dc6d041
fix(deref): resolve component-form $dynamicRef
aqeelat Jul 29, 2026
0d7d715
Merge pull request #508 from aqeelat/fix/dynamic-ref-component-form-d…
mattpolzin Jul 30, 2026
576e309
Merge branch 'main' into release/7_0
mattpolzin Aug 31, 2026
e1453c1
remove Validation system from OpenAPIKit30
mattpolzin Aug 31, 2026
62d15f2
Merge pull request #511 from mattpolzin/remove-oas30-validation-system
mattpolzin Aug 31, 2026
2cdf4a2
remove simplification support from OpenAPIKit30 module
mattpolzin Aug 31, 2026
d83e841
Merge pull request #512 from mattpolzin/remove-oas30-simplification
mattpolzin Aug 31, 2026
cc2c861
update docs given removal of OAS 3.0 simplification and validation code
mattpolzin Aug 31, 2026
c3fa627
link to the readme for info on reading 3.0 docs but working against t…
mattpolzin Sep 8, 2026
1c1bb9d
give the external loader component key function the full loaded objec…
mattpolzin Sep 8, 2026
f4c0ab8
update documentation and migration guide
mattpolzin Sep 8, 2026
895e725
Merge pull request #513 from mattpolzin/feature/438/loader-improvement
mattpolzin Sep 8, 2026
4aca8f4
syntactic change that addresses compiler warning
mattpolzin Sep 8, 2026
7e12979
Merge branch 'main' into release/7_0
mattpolzin Sep 10, 2026
11d531a
adopt OAS 3.2.1 version as a breaking change for OpenAPIKit v7
mattpolzin Sep 10, 2026
51c069d
Merge pull request #518 from mattpolzin/oas-3-2-1-breaking-changes
mattpolzin Sep 11, 2026
c195867
Merge branch 'main' into release/7_0
mattpolzin Sep 14, 2026
51d33d9
update migration guide
mattpolzin Sep 14, 2026
f6b4dfb
add note about default document version
mattpolzin Sep 14, 2026
9f4a927
add a note about validation/simplification in OpenAPIKit 7 directly t…
mattpolzin Sep 14, 2026
cec1602
tweak version support table
mattpolzin Sep 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/codecov.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ on:
jobs:
codecov:
container:
image: swift:6.2
image: swift:6.3
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/documentation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ jobs:
build:
runs-on: ubuntu-latest
container:
image: swift:6.2
image: swift:6.3

steps:
- uses: actions/checkout@v5
Expand Down
3 changes: 0 additions & 3 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,6 @@ jobs:
fail-fast: false
matrix:
image:
- swift:6.1-focal
- swift:6.1-jammy
- swift:6.1-noble
- swift:6.2-jammy
- swift:6.2-noble
- swift:6.3-jammy
Expand Down
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// swift-tools-version: 6.1
// swift-tools-version: 6.2

import PackageDescription

Expand Down
47 changes: 28 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,18 @@ OpenAPIKit follows semantic versioning despite the fact that the OpenAPI
specificaiton does not. The following chart shows which OpenAPI specification
versions and key features are supported by which OpenAPIKit versions.

| OpenAPIKit | Swift | OpenAPI v3.0, v3.1 | OpenAPI v3.2 | Package Traits |
|------------|-------|--------------------|--------------|----------------|
| v4.x | 5.8+ | ✅ | | |
| v5.x | 5.10+ | ✅ | ✅ | |
| v6.x | 6.1+ | ✅ | ✅ | ✅ |
| OpenAPIKit | Swift | OpenAPI v3.0 - v3.2 | Dynamic Anchors/References |
|------------|-------|---------------------|----------------------------|
| v5.x | 5.10+ | ✅ | |
| v6.x | 6.1+ | ✅ | |
| v7.x | 6.2+ | ✅ | ✅ |

- [Usage](#usage)
- [Migration](#migration)
- [Older Versions](#older-versions)
- [3.x to 4.x](#3x-to-4x)
- [4.x to 5.x](#4x-to-5x)
- [5.x to 6.x](#5x-to-6x)
- [6.x to 7.x](#6x-to-7x)
- [Decoding OpenAPI Documents](#decoding-openapi-documents)
- [Decoding Errors](#decoding-errors)
- [Encoding OpenAPI Documents](#encoding-openapi-documents)
Expand Down Expand Up @@ -58,13 +58,7 @@ versions and key features are supported by which OpenAPIKit versions.
#### Older Versions
- [`1.x` to `2.x`](./documentation/migration_guides/v2_migration_guide.md)
- [`2.x` to `3.x`](./documentation/migration_guides/v3_migration_guide.md)

#### 3.x to 4.x
If you are migrating from OpenAPIKit 3.x to OpenAPIKit 4.x, check out the
[v4 migration guide](./documentation/migration_guides/v4_migration_guide.md).

Be aware of the changes to minimum Swift version and minimum Yams version
(although Yams is only a test dependency of OpenAPIKit).
- [`3.x` to `4.x`](./documentation/migration_guides/v4_migration_guide.md)

#### 4.x to 5.x
If you are migrating from OpenAPIKit 4.x to OpenAPIKit 5.x, check out the
Expand All @@ -78,6 +72,17 @@ If you are migrating from OpenAPIKit 5.x to OpenAPIKit 6.x, check out the

Be aware of the change to minimum Swift version, now Swift 6.1.

#### 6.x to 7.x
If you are migrating from OpenAPIKit 6.x to OpenAPIKit 7.x, check out the
[v7 migration guide](./documentation/migration_guides/v7_migration_guide.md).

Be aware of the change to minimum Swift version, now Swift 6.2.

This version drops support for validations and simplification against OAS 3.0.x
documents. You can convert those documents to OAS 3.1/3.2 in-code and run
validations against the newer versions which is generally going to be quite
adequate. See the migration guide for more.

### Decoding OpenAPI Documents

Most documentation will focus on what it looks like to work with the
Expand Down Expand Up @@ -177,6 +182,11 @@ 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](./documentation/validation.md).

Validations can only be run on OAS 3.1 or OAS 3.2 documents but you can convert
OAS 3.0 documents to 3.1 or 3.2 using OpenAPIKit and run validations on that
result to effectively validate OAS 3.0 documents. See the next section for
details on converting OAS 3.0 documents to later versions.

### Supporting OpenAPI 3.0.x Documents
If you need to operate on OpenAPI 3.0.x documents and only 3.0.x documents, you
can use the `OpenAPIKit30` module throughout your code.
Expand Down Expand Up @@ -589,7 +599,7 @@ OpenAPIKit leaves it to you to decide how to load external files and where to
store the results in the Components Object. It does this by requiring that you
provide an implementation of the
[`ExternalLoader`](https://mattpolzin.github.io/OpenAPIKit/documentation/openapikit/externalloader)
protocol. You provide a `load` function and a `componentKey` function, both of
protocol. You provide a `load` function and a `componentKey()` function, both of
which accept as input the `URL` to load. A simple mock example implementation
from the OpenAPIKit tests will go a long way to showing how the `ExternalLoader`
can be set up:
Expand All @@ -600,14 +610,13 @@ struct ExampleLoader: ExternalLoader {

static func load<T>(_ url: URL) async throws -> (T, [Message]) where T : Decodable {
// load data from file, perhaps. we will just mock that up for the example:
let data = try await mockData(componentKey(type: T.self, at: url))
let data = try await mockData(url)

// We use the YAML decoder purely for order-stability.
let decoded = try YAMLDecoder().decode(T.self, from: data)
let finished: T
// while unnecessary, a loader may likely want to attatch some extra info
// to keep track of where a reference was loaded from. This example
shows
// while unnecessary, a loader may likely want to attach some extra info
// to keep track of where a reference was loaded from. This example shows
// the strategy of using vendor extensions.
if var extendable = decoded as? VendorExtendable {
extendable.vendorExtensions["x-source-url"] = AnyCodable(url)
Expand All @@ -618,7 +627,7 @@ struct ExampleLoader: ExternalLoader {
return (finished, [])
}

static func componentKey<T>(type: T.Type, at url: URL) throws -> OpenAPIKit.OpenAPI.ComponentKey {
static func componentKey<T>(for object: T, at url: URL) throws -> OpenAPIKit.OpenAPI.ComponentKey {
// do anything you want here to determine what key the new component should be stored at.
//
// for the example, we will just transform the URL path into a valid components key:
Expand Down
24 changes: 22 additions & 2 deletions Sources/OpenAPIKit/Document/Document.swift
Original file line number Diff line number Diff line change
Expand Up @@ -474,20 +474,22 @@ extension OpenAPI.Document {
case v3_1_x(x: Int)

case v3_2_0
case v3_2_1
case v3_2_x(x: Int)

// Generally it makes sense for new documents to default to the latest
// released version OpenAPIKit supports but to be safe we will avoid
// the "breaking change" of the default version changing other than
// with major releases.
public static let defaultVersion = Self.v3_2_0
public static let defaultVersion = Self.v3_2_1

public init?(rawValue: String) {
switch rawValue {
case "3.1.0": self = .v3_1_0
case "3.1.1": self = .v3_1_1
case "3.1.2": self = .v3_1_2
case "3.2.0": self = .v3_2_0
case "3.2.1": self = .v3_2_1
default:
let components = rawValue.split(separator: ".")
guard components.count == 3 else {
Expand All @@ -506,7 +508,7 @@ extension OpenAPI.Document {
let v3_1PatchUpperBound = 2
let v3_2PatchUpperBound = 1
if minorVersion == "2" {
guard patchVersion > 0 && patchVersion <= v3_2PatchUpperBound else {
guard patchVersion > 1 && patchVersion <= v3_2PatchUpperBound else {
return nil
}
self = .v3_2_x(x: patchVersion)
Expand All @@ -527,6 +529,7 @@ extension OpenAPI.Document {
case .v3_1_x(x: let x): return "3.1.\(x)"

case .v3_2_0: return "3.2.0"
case .v3_2_1: return "3.2.1"
case .v3_2_x(x: let x): return "3.2.\(x)"
}
}
Expand All @@ -540,6 +543,7 @@ extension OpenAPI.Document {
case .v3_1_2: true
case .v3_1_x(x: let x): 0 < x
case .v3_2_0: true
case .v3_2_1: true
case .v3_2_x(x: _): true
}

Expand All @@ -550,6 +554,7 @@ extension OpenAPI.Document {
case .v3_1_2: true
case .v3_1_x(x: let y): 1 < y
case .v3_2_0: true
case .v3_2_1: true
case .v3_2_x(x: _): true
}

Expand All @@ -560,6 +565,7 @@ extension OpenAPI.Document {
case .v3_1_2: false
case .v3_1_x(x: let y): 2 < y
case .v3_2_0: true
case .v3_2_1: true
case .v3_2_x(x: _): true
}

Expand All @@ -570,6 +576,7 @@ extension OpenAPI.Document {
case .v3_1_2: x < 2
case .v3_1_x(x: let y): x < y
case .v3_2_0: true
case .v3_2_1: true
case .v3_2_x(x: _): true
}

Expand All @@ -580,16 +587,29 @@ extension OpenAPI.Document {
case .v3_1_2: false
case .v3_1_x(x: _): false
case .v3_2_0: false
case .v3_2_1: true
case .v3_2_x(x: let y): 0 < y
}

case .v3_2_1:
switch rhs {
case .v3_1_0: false
case .v3_1_1: false
case .v3_1_2: false
case .v3_1_x(x: _): false
case .v3_2_0: false
case .v3_2_1: false
case .v3_2_x(x: let y): 1 < y
}

case .v3_2_x(x: let x):
switch rhs {
case .v3_1_0: false
case .v3_1_1: false
case .v3_1_2: false
case .v3_1_x(x: _): false
case .v3_2_0: x < 0
case .v3_2_1: x < 1
case .v3_2_x(x: let y): x < y
}
}
Expand Down
2 changes: 1 addition & 1 deletion Sources/OpenAPIKit/ExternalLoader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ public protocol ExternalLoader: _ExternalLoaderMetatype where Message: Sendable
/// but the same key for all equal objects. In practice, this probably means that any
/// time the same type and URL pair are passed in the same `ComponentKey` should be
/// returned.
static func componentKey<T>(type: T.Type, at url: URL) throws -> OpenAPI.ComponentKey
static func componentKey<T>(for object: T, at url: URL) throws -> OpenAPI.ComponentKey
}

public protocol ExternallyDereferenceable {
Expand Down
107 changes: 107 additions & 0 deletions Sources/OpenAPIKit/JSONDynamicReference.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import OpenAPIKitCore

#if canImport(FoundationEssentials)
import FoundationEssentials
#else
import Foundation
#endif

/// A `JSONDynamicReference` represents a JSON Schema `$dynamicRef`
/// (JSON Schema 2020-12, [§7.7](https://json-schema.org/draft/2020-12/json-schema-core#section-7.7)).
///
/// Like `JSONReference`, a dynamic reference can point either to a component
/// in the Components Object, to another location within the same document
/// (including a `$dynamicAnchor`), or to another file.
///
/// OpenAPIKit parses and round-trips `$dynamicRef`. Dynamic-scope *evaluation*
/// is a runtime concern belonging to JSON Schema validators; local
/// dereferencing (`locallyDereferenced()`) does not resolve `$dynamicRef` and
/// fails if it encounters one that cannot be inlined.
@dynamicMemberLookup
public struct JSONDynamicReference: Equatable, Hashable, Sendable {
public let jsonReference: JSONReference<JSONSchema>

public init(_ reference: JSONReference<JSONSchema>) {
self.jsonReference = reference
}

public subscript<T>(dynamicMember path: KeyPath<JSONReference<JSONSchema>, T>) -> T {
return jsonReference[keyPath: path]
}

/// Reference a `$dynamicAnchor` (or `$anchor`) local to this document.
///
/// - Important: `anchor` does not contain a leading '#'.
public static func anchor(_ anchor: String) -> Self {
return .init(.internal(.anchor(anchor)))
}
}

// MARK: - Codable

extension JSONDynamicReference {
private enum CodingKeys: String, CodingKey {
case dynamicRef = "$dynamicRef"
}
}

extension JSONDynamicReference: Encodable {
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)

switch jsonReference {
case .internal(let reference):
try container.encode(reference.rawValue, forKey: .dynamicRef)
case .external(let url):
try container.encode(url.absoluteString, forKey: .dynamicRef)
}
}
}

extension JSONDynamicReference: Decodable {
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)

let referenceString = try container.decode(String.self, forKey: .dynamicRef)

guard !referenceString.isEmpty else {
throw DecodingError.dataCorruptedError(forKey: .dynamicRef, in: container, debugDescription: "Expected a reference string, but found an empty string instead.")
}

if referenceString.first == "#" {
guard let internalReference = JSONReference<JSONSchema>.InternalReference(rawValue: referenceString) else {
throw GenericError(
subjectName: "JSON Dynamic Reference",
details: "Failed to parse a JSON Dynamic Reference from '\(referenceString)'",
codingPath: container.codingPath
)
}
self = .init(.internal(internalReference))
} else {
let externalReference: URL?
#if canImport(FoundationEssentials)
externalReference = URL(string: referenceString, encodingInvalidCharacters: false)
#elseif os(macOS) || os(iOS) || os(watchOS) || os(tvOS)
if #available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) {
externalReference = URL(string: referenceString, encodingInvalidCharacters: false)
} else {
externalReference = URL(string: referenceString)
}
#else
externalReference = URL(string: referenceString)
#endif
guard let externalReference else {
throw GenericError(
subjectName: "JSON Dynamic Reference",
details: "Failed to parse a valid URI for a JSON Dynamic Reference from '\(referenceString)'",
codingPath: container.codingPath
)
}
self = .init(.external(externalReference))
}
}
}

// Conforms for parity with JSONReference; lets downstream code key
// Validations on dynamic references (a `Validation`'s `Subject` must be `Validatable`).
extension JSONDynamicReference: Validatable {}
Loading
Loading