Swiftpack.co - Package - mattpolzin/JSONAPI

JSONAPI

MIT license Swift 5.1 Build Status

A Swift package for encoding to- and decoding from JSON API compliant requests and responses.

See the JSON API Spec here: https://jsonapi.org/format/

:warning: This library provides well-tested type safety when working with JSON:API 1.0. However, the Swift compiler can sometimes have difficulty tracking down small typos when initializing ResourceObjects. Correct code will always compile, but tracking down the source of programmer errors can be an annoyance. This is mostly a concern when creating resource objects in-code (i.e. declaratively) like you might for unit testing. Writing a client that uses this framework to ingest and decode JSON API Compliant API responses is much less painful.

Quick Start

:warning: The following Google Colab examples have correct code, but from time to time the Google Colab Swift compiler may be buggy and produce incorrect or erroneous results. Just keep that in mind if you run the code as you read through the Colab examples.

Clientside

Serverside

Client+Server

This library works well when used by both the server responsible for serialization and the client responsible for deserialization. Check out the example further down in this README.

Table of Contents

Primary Goals

The primary goals of this framework are:

  1. Allow creation of Swift types that are easy to use in code but also can be encoded to- or decoded from JSON API v1.0 Spec compliant payloads without lots of boilerplate code.
  2. Leverage Codable to avoid additional outside dependencies and get operability with non-JSON encoders/decoders for free.
  3. Do not sacrifice type safety.
  4. Be platform agnostic so that Swift code can be written once and used by both the client and the server.
  5. Provide human readable error output. The errors thrown when decoding an API response and the results of the JSONAPITesting framework's compare(to:) functions all have digestible human readable descriptions (just use String(describing:)).

Caveat

The big caveat is that, although the aim is to support the JSON API spec, this framework ends up being naturally opinionated about certain things that the API Spec does not specify. These caveats are largely a side effect of attempting to write the library in a "Swifty" way.

If you find something wrong with this library and it isn't already mentioned under Project Status, let me know! I want to keep working towards a library implementation that is useful in any application.

Dev Environment

Prerequisites

  1. Swift 5.1+
  2. Swift Package Manager, Xcode 11+, or Cocoapods

Swift Package Manager

Just include the following in your package's dependencies and add JSONAPI to the dependencies for any of your targets.

	.package(url: "https://github.com/mattpolzin/JSONAPI.git", .upToNextMajor(from: "3.0.0"))

Xcode project

To create an Xcode project for JSONAPI, run swift package generate-xcodeproj

With Xcode 11+ you can also just open the folder containing your clone of this repository and begin working.

CocoaPods

To use this framework in your project via Cocoapods, add the following dependencies to your Podfile.

	pod 'Poly', :git => 'https://github.com/mattpolzin/Poly.git'
	pod 'MP-JSONAPI', :git => 'https://github.com/mattpolzin/JSONAPI.git'

Running the Playground

To run the included Playground files, create an Xcode project using Swift Package Manager, then create an Xcode Workspace in the root of the repository and add both the generated Xcode project and the playground to the Workspace.

Note that Playground support for importing non-system Frameworks is still a bit touchy as of Swift 4.2. Sometimes building, cleaning and building, or commenting out and then uncommenting import statements (especially in the Entities.swift Playground Source file) can get things working for me when I am getting an error about JSONAPI not being found.

Project Status

JSON:API

Document

  • data
  • included
  • errors
  • meta
  • jsonapi (i.e. API Information)
  • links

Resource Object

  • id
  • type
  • attributes
  • relationships
  • links
  • meta

Relationship Object

  • data
  • links
  • meta

Links Object

  • href
  • meta

Misc

  • ☑ Support transforms on Attributes values (e.g. to support different representations of Date)
  • ☑ Support validation on Attributes.
  • ☑ Support sparse fieldsets (encoding only). A client can likely just define a new model to represent a sparse population of another model in a very specific use case for decoding purposes. On the server side, sparse fieldsets of Resource Objects can be encoded without creating one model for every possible sparse fieldset.

Testing

Resource Object Validator

  • ☑ Disallow optional array in Attribute (should be empty array, not null).
  • ☑ Only allow TransformedAttribute and its derivatives as stored properties within Attributes struct. Computed properties can still be any type because they do not get encoded or decoded.
  • ☑ Only allow ToManyRelationship and ToOneRelationship within Relationships struct.

Potential Improvements

These ideas could be implemented in future versions.

  • ☐ (Maybe) Use KeyPath to specify Includes thus creating type safety around the relationship between a primary resource type and the types of included resources.
  • ☐ (Maybe) Replace SingleResourceBody and ManyResourceBody with support at the Document level to just interpret PrimaryResource, PrimaryResource?, or [PrimaryResource] as the same decoding/encoding strategies.
  • ☐ Support sideposting. JSONAPI spec might become opinionated in the future (https://github.com/json-api/json-api/pull/1197, https://github.com/json-api/json-api/issues/1215, https://github.com/json-api/json-api/issues/1216) but there is also an existing implementation to consider (https://jsonapi-suite.github.io/jsonapi_suite/ruby/writes/nested-writes). At this time, any sidepost implementation would be an awesome tertiary library to be used alongside the primary JSONAPI library. Maybe JSONAPISideloading.
  • ☐ Error or warning if an included resource object is not related to a primary resource object or another included resource object (Turned off or at least not throwing by default).

Example

The following serves as a sort of pseudo-example. It skips server/client implementation details not related to JSON:API but still gives a more complete picture of what an implementation using this framework might look like. You can play with this example code in the Playground provided with this repo.

Preamble (Setup shared by server and client)

// Make String a CreatableRawIdType.
var globalStringId: Int = 0
extension String: CreatableRawIdType {
	public static func unique() -> String {
		globalStringId += 1
		return String(globalStringId)
	}
}

// Create a typealias because we do not expect JSON:API Resource
// Objects for this particular API to have Metadata or Links associated
// with them. We also expect them to have String Identifiers.
typealias JSONEntity<Description: ResourceObjectDescription> = JSONAPI.ResourceObject<Description, NoMetadata, NoLinks, String>

// Similarly, create a typealias for unidentified entities. JSON:API
// only allows unidentified entities (i.e. no "id" field) for client
// requests that create new entities. In these situations, the server
// is expected to assign the new entity a unique ID.
typealias UnidentifiedJSONEntity<Description: ResourceObjectDescription> = JSONAPI.ResourceObject<Description, NoMetadata, NoLinks, Unidentified>

// Create relationship typealiases because we do not expect
// JSON:API Relationships for this particular API to have
// Metadata or Links associated with them.
typealias ToOneRelationship<Entity: Identifiable> = JSONAPI.ToOneRelationship<Entity, NoMetadata, NoLinks>
typealias ToManyRelationship<Entity: Relatable> = JSONAPI.ToManyRelationship<Entity, NoMetadata, NoLinks>

// Create a typealias for a Document because we do not expect
// JSON:API Documents for this particular API to have Metadata, Links,
// useful Errors, or an APIDescription (The *SPEC* calls this
// "API Description" the "JSON:API Object").
typealias Document<PrimaryResourceBody: JSONAPI.CodableResourceBody, IncludeType: JSONAPI.Include> = JSONAPI.Document<PrimaryResourceBody, NoMetadata, NoLinks, IncludeType, NoAPIDescription, BasicJSONAPIError<String>>

// MARK: Entity Definitions

enum AuthorDescription: ResourceObjectDescription {
	public static var jsonType: String { return "authors" }

	public struct Attributes: JSONAPI.Attributes {
		public let name: Attribute<String>
	}

	public typealias Relationships = NoRelationships
}

typealias Author = JSONEntity<AuthorDescription>

enum ArticleDescription: ResourceObjectDescription {
	public static var jsonType: String { return "articles" }

	public struct Attributes: JSONAPI.Attributes {
		public let title: Attribute<String>
		public let abstract: Attribute<String>
	}

	public struct Relationships: JSONAPI.Relationships {
		public let author: ToOneRelationship<Author>
	}
}

typealias Article = JSONEntity<ArticleDescription>

// MARK: Document Definitions

// We create a typealias to represent a document containing one Article
// and including its Author
typealias SingleArticleDocumentWithIncludes = Document<SingleResourceBody<Article>, Include1<Author>>

// ... and a typealias to represent a document containing one Article and
// not including any related entities.
typealias SingleArticleDocument = Document<SingleResourceBody<Article>, NoIncludes>

Server Pseudo-example

// Skipping over all the API and database stuff, here's a chunk of code
// that creates a document. Note that this document is the entirety
// of a JSON:API response body.
func articleDocument(includeAuthor: Bool) -> Either<SingleArticleDocument, SingleArticleDocumentWithIncludes> {
    // Let's pretend all of this is coming from a database:

    let authorId = Author.Identifier(rawValue: "1234")

    let article = Article(id: .init(rawValue: "5678"),
                          attributes: .init(title: .init(value: "JSON:API in Swift"),
                                            abstract: .init(value: "Not yet written")),
                          relationships: .init(author: .init(id: authorId)),
                          meta: .none,
                          links: .none)

    let document = SingleArticleDocument(apiDescription: .none,
                                         body: .init(resourceObject: article),
                                         includes: .none,
                                         meta: .none,
                                         links: .none)

    switch includeAuthor {
    case false:
        return .init(document)

    case true:
        let author = Author(id: authorId,
                            attributes: .init(name: .init(value: "Janice Bluff")),
                            relationships: .none,
                            meta: .none,
                            links: .none)

        let includes: Includes<SingleArticleDocumentWithIncludes.Include> = .init(values: [.init(author)])

        return .init(document.including(includes))
    }
}

let encoder = JSONEncoder()
encoder.keyEncodingStrategy = .convertToSnakeCase
encoder.outputFormatting = .prettyPrinted

let responseBody = articleDocument(includeAuthor: true)
let responseData = try! encoder.encode(responseBody)

// Next step would be setting the HTTP body of a response.
// We will just print it out instead:
print("-----")
print(String(data: responseData, encoding: .utf8)!)

// ... and if we had received a request for an article without
// including the author:
let otherResponseBody = articleDocument(includeAuthor: false)
let otherResponseData = try! encoder.encode(otherResponseBody)
print("-----")
print(String(data: otherResponseData, encoding: .utf8)!)

Client Pseudo-example

enum NetworkError: Swift.Error {
    case serverError
    case quantityMismatch
}

// Skipping over all the API stuff, here's a chunk of code that will
// decode a document. We will assume we have made a request for a
// single article including the author.
func docode(articleResponseData: Data) throws -> (article: Article, author: Author) {
    let decoder = JSONDecoder()
    decoder.keyDecodingStrategy = .convertFromSnakeCase

    let articleDocument = try decoder.decode(SingleArticleDocumentWithIncludes.self, from: articleResponseData)

    switch articleDocument.body {
    case .data(let data):
        let authors = data.includes[Author.self]

        guard authors.count == 1 else {
            throw NetworkError.quantityMismatch
        }

        return (article: data.primary.value, author: authors[0])
    case .errors(let errors, meta: _, links: _):
        throw NetworkError.serverError
    }
}

let response = try! docode(articleResponseData: responseData)

// Next step would be to do something useful with the article and author but we will print them instead.
print("-----")
print(response.article)
print(response.author)

Deeper Dive

See the usage documentation.

JSONAPI+Testing

The JSONAPI framework is packaged with a test library to help you test your JSONAPI integration. The test library is called JSONAPITesting. You can see JSONAPITesting in action in the Playground included with the JSONAPI repository.

Literal Expressibility

Literal expressibility for Attribute, ToOneRelationship, and Id are provided so that you can easily write test ResourceObject values into your unit tests.

For example, you could create a mock Author (from the above example) as follows

let author = Author(
	id: "1234", // You can just use a String directly as an Id
	attributes: .init(name: "Janice Bluff"), // The name Attribute does not need to be initialized, you just use a String directly.
	relationships: .none,
	meta: .none,
	links: .none
)

Resource Object check()

The ResourceObject gets a check() function that can be used to catch problems with your JSONAPI structures that are not caught by Swift's type system.

To catch malformed JSONAPI.Attributes and JSONAPI.Relationships, just call check() in your unit test functions:

func test_initAuthor() {
	let author = Author(...)
	Author.check(author)
}

Comparisons

You can compare Documents, ResourceObjects, Attributes, etc. and get human-readable output using the compare(to:) methods included with JSONAPITesting.

func test_articleResponse() {
	let endToEndAPITestResponse: SingleArticleDocumentWithIncludes = ...

	let expectedResponse: SingleArticleDocumentWithIncludes = ...

	let comparison = endToEndAPITestResponse.compare(to: expectedResponse)

	XCTAssert(comparison.isSame, String(describing: comparison))
}

JSONAPI+Arbitrary

The JSONAPI+Arbitrary library provides SwiftCheck Arbitrary conformance for many of teh JSONAPI types.

See https://github.com/mattpolzin/JSONAPI-Arbitrary for more information.

JSONAPI+OpenAPI

The JSONAPI+OpenAPI library generates OpenAPI compliant JSON Schema for models built with the JSONAPI library. If your Swift code is your preferred source of truth for API information, this is an easy way to document the response schemas of your API.

JSONAPI+OpenAPI also has experimental support for generating JSONAPI Swift code from Open API documentation (this currently lives on the feature/gen-swift branch).

See https://github.com/mattpolzin/JSONAPI-OpenAPI for more information.

Github

link
Stars: 21

Dependencies

Releases

New testing feature and _much_ better errors - 2019-11-16 07:59:22

  • Adds JSONAPITesting framework compare(to:) functions that deliver concise comparisons of two documents or resource objects. Just print the String(describing:) a comparison to get a human readable description of the differences.
  • Adds ResourceObjectDecodingError and DocumentDecodingError which represent more JSON:API-specific errors and come with good string descriptions for many common reasons why JSON:API resources or documents could fail to decode. Just catch the error from decoding a ResourceObject or Document and save or print String(describing: error).

⚠️ Breaking Changes ⚠️

📛 Renames JSONAPIDocument to CodableJSONAPIDocument. 📛 Renames OptionalPrimaryResource to OptionalCodablePrimaryResource. 📛 Renames PrimaryResource to CodablePrimaryResource. 📛 Renames ResourceBody to CodableResourceBody. 📛 Renames PolyWrapped to CodablePolyWrapped. 📛 Renames SingleResourceBody.Entity to SingleResourceBody.PrimaryResource 📛 Renames ManyResourceBody.Entity to ManyResourceBody.PrimaryResource 📛 Renames JSONAPIEncodingError to JSONAPICodingError

⛔️ Deprecates subscript access of ResourceObject attributes in favor of key path dynamic member lookup.

🗑 Removes key path dynamic member lookup of Document properties on Document.SuccessDocument and Document.ErrorDocument in favor of directly providing relevant accessors (see SuccessDocument/ErrorDocument for details).

Bug fixes for JSONAPITesting compare(to:) - 2019-11-16 01:10:30

Various bug fixes for compare(to:) along with improved test coverage.

Mostly finished JSONAPITesting comparison API and improved errors - 2019-11-13 02:43:40

APIs should not change much during beta, just improving test coverage and fixing bugs.

Added much more descriptive error handling - 2019-11-11 07:48:41

Added much more descriptive error handling for decoding of Resource Objects and JSON:API Documents.

DecodingError and JSONAPICodingError replaced by ResourceObjectDecodingError and DocumentDecodingError where appropriate in order to get more concise JSON:API-targeted error reporting. Any error handling that specifically targets DecodingError coming out of the JSONAPI framework will need to be revisited.

Reduce `Document` `compare(to:)` footprint. - 2019-11-07 07:50:07

Add associated type for the primary resource on the primary resource body protocols.

Drastically reduce the footprint of document compare(to:) functions by using the new associated type.

Abstract away the types for document comparison - 2019-11-06 08:25:28

1st alpha release of 3.0.0 - 2019-11-06 07:14:12

This release will:

  1. Add compare(to:) API to JSONAPITesting framework to produce much easier to read diagnostics when two ResourceObjects or Documents that are expected to be the same actually have differences.
  2. Bring breaking changes around type names and APIs in the interest of usability.
  3. Deprecate ResourceObject subscript access to attributes in favor of key path dynamic member lookup.

Test coverage and documentation will be improved over the alpha releases in addition to bugs being fixed and additional (potentially breaking) changes being introduced.

Fix return type of `Document.SuccessDocument.including()` - 2019-10-27 23:32:56

Fixed:

  • In 2.5.0, Document.SuccessDocument.including() returned a Document. That has been fixed in 2.5.1 to return a Document.SuccessDocument.

Add Document.ErrorDocument and Document.SuccessDocument - 2019-10-21 05:30:52

Add two new types that guarantee either success or failure for a Document body. These types can be used in situations where you want to let the type system know a Document will have a data or error body prior to the Document being initialized.

Adding tapping/replacing on attributes and relationships - 2019-10-13 02:33:03

Adds tappingAttributes(), replacingAttributes(), tappingRelationships(), and replacingRelationships() to ResourceObject.

Each method returns a new ResourceObject with replaced or mutated Attributes or Relationships.

See https://github.com/mattpolzin/JSONAPI#replacing-and-tapping-attributesrelationships for more details.

Add Include11 Type - 2019-10-03 03:11:38

Add a type that can represent one of 11 different possible included types.

Additional error types! - 2019-09-30 00:11:27

The GenericJSONAPIError makes it easy to fit any old structure to the requirements for the JSONAPIError protocol.

The BasicJSONAPIError provides out-of-box support for parsing most of the fields the JSON:API Spec says might be available on error objects. These fields are all optional because the Spec does not require any of them to be present.

More than likely, transitioning from use of UnknownJSONAPIError to BasicJSONAPIError<String> (or perhaps specialized on Int) is an easy non-breaking move for most codebases that gets you more information about the errors being parsed.

Add Include10 - 2019-09-17 00:15:02

Add Include10 type that allows you to include 10 different types of resources with a JSONAPI.Document.

Change CocoaPods spec name and add new way to access attributes - 2019-09-14 23:42:51

New with this version is dynamicMemberLookup of JSON:API attributes. You can still access a model's attributes as model[\.attributeName] but now you can also just write model.attributeName. Note this applies to attributes, not relationships.

⚠️ Breaking Changes ⚠️

  • Swift Tools version 5.1 is required for SPM
  • Swift 5.1 is required for the dynamicMemberLookup feature allowing more concise access to JSON:API attributes.
  • The PodSpec name of this library has changed to avoid a name conflict with a different JSONAPI library -- the module name remains the same, so your imports don't change, just your pod file dependency declaration.

Sparse Fieldset support and first major version release. - 2019-08-15 00:46:04

This release adds support for encoding sparse fieldsets of ResourceObjects.

⚠️ Breaking Changes ⚠️

The following breaking changes were mostly made to support sparse fieldsets as an encoding-only feature.

  • JSONAPI.Include went from guaranteeing Codable to only guaranteeing Encodable, although conforming types within the library are still conditionally Decodable.
  • The JSONPoly typealias was replaced by EncodableJSONPoly and only guarantees Encodable.
  • AppendableResourceBody was renamed Appendable and does not guarantee conformance to ResourceBody anymore (but ManyResourceBody conforms to both ResourceBody and Appendable).
  • MaybePrimaryResource was renamed OptionalPrimaryResource.

CocoaPods fixes - 2019-07-17 05:21:47

Allow relationship object omission if all relationships are optional - 2019-07-03 14:49:16

⚠️ Breaking-ish change ⚠️

If you had previously come to rely on the fact that the only way to allow omission of the relationships key was to create the Relationships = NoRelationships type alias, this change will be breaking. Still, because the JSON:API Spec allows for omission of the relationships key unconditionally, this change does bring the library more in line with the spec and I think this library's usability improves.

A lot of renaming to better align with JSON:API spec language. - 2019-06-13 04:01:47

⚠️ Breaking change ⚠️

Pretty large renaming effort to better align the type names in this library with the names used by the JSON:API Spec. I think this will ultimately make it much easier to adopt this library given prior knowledge of the Spec. There are no substantive feature changes here, just renaming.

For the most part, renaming is of the pattern: If it used to be Entity then it is now ResourceObject

Change spelling of "direct" subscript overload on Entity. - 2019-04-19 06:10:01

⚠️ Breaking change ⚠️

Take the following definitions:

enum TestDescription: EntityDescription {
  static var jsonType: String { return "test" }

  typealias Relationships = NoRelationships

  struct Attributes: JSONAPI.Attributes {
    let normal: Attribute<String>
    var direct: String { return normal.value }
  }
}
typealias Test = Entity<TestDescription, NoMetadata, NoLinks, String>

let test: Test = ...

You used to be able to access both attributes using the default subscript:

let one: String = test[\.normal]
let two = test[\.direct]

However, in the case of test[\.normal], the code was ambiguous unless you explicitly asked for a String (the compiler would not know if you wanted the String or the whole Attribute<String>).

That is what has changed. There is no more ambiguity, but now the direct access subscript has a new spelling:

let one = test[\.normal]
let two = test[direct: \.direct]

It may seem like the verbosity of the code has just moved from one place to another, but the much less common feature (direct access) has taken on the burden.

Default to Swift version 5.0 - 2019-04-17 05:36:33

⚠️ Breaking Change ⚠️
Requires Swift Tools version 5.0 (shipped with Xcode 10.2)

Add CocoaPods support - 2019-04-15 04:02:18

See README update for details.

Documentation clarifications - 2019-02-08 02:57:48

Documentation clarifications and the removal of some redundant public scoping for functions within public extensions. These become warnings with the Swift 5 compiler.

Remove Playground page that depended on JSONAPI+OpenAPI - 2019-01-31 02:35:52

Moved JSONAPI+OpenAPI and JSONAPI+Arbitrary into their own Packages - 2019-01-31 02:02:46

You can find them at https://github.com/mattpolzin/JSONAPI-OpenAPI and https://github.com/mattpolzin/JSONAPI-Arbitrary

OpenAPI Playground fix - 2019-01-28 06:56:04

Bug Fixes and test additions (OpenAPI support only) - 2019-01-28 06:48:18

Fixed a few bugs, including Included support for documents. Added some tests around that and more.

OpenAPI Bug fixes and added support for Operation.RequestBody - 2019-01-27 21:58:36

Now a requestBody key can be specified on an OpenAPI "operation." Like most of the rest of the OpenAPI support, this is currently untested.

This does not affect the stability of the rest of the JSONAPI project.

Fix another dictionary-as-array bug. - 2019-01-27 04:05:01

Fix two bugs - 2019-01-27 03:53:20

Fix:

  1. OpenAPI schema for Entity included id when the Entity was "Unidentified."
  2. OpenAPI schema was invalid due to two Dictionaries getting encoded as arrays.

Bringing in a lot of untested OpenAPI Stuff - 2019-01-26 02:27:54

Bringing a lot of untested and far from completed OpenAPI stuff into the mix.

  1. It is still useful in the right context.
  2. It does not touch or destabilize the JSONAPI work.
  3. It all exists within the JSONAPIOpenAPI framework and does not even need to be built.