Swiftpack.co - Package - mattpolzin/JSONAPI

JSONAPI

MIT license Swift 5.2 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/

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 claim it cannot build the JSONAPI library.

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.

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.2+
  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", from: "4.0.0")

Xcode project

With Xcode 11+, you can open the folder containing this repository. There is no need for an Xcode project, but you can generate one with swift package generate-xcodeproj.

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 theEntities.swift Playground Source file) can get things working for me when I am getting an error about JSONAPI not being found.

Deeper Dive

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

JSONAPI-ResourceStorage

The JSONAPI-ResourceStorage package has two very early stage modules supporting storage and retrieval of JSONAPI.ResourceObjects. Please consider these modules to be more of examples of two directions you could head in than anything else.

https://github.com/mattpolzin/JSONAPI-ResourceStorage

Github

link
Stars: 41

Dependencies

Releases

Version 5.0.0 Release Candidate 1 - 2020-09-22 03:52:28

This version has drastically clearer error reporting around failures to decode includes in a JSON:API document (thanks @mlomeli for the help in designing a better experience).

This version also adds support for metadata within Resource Identifier Objects. You were already able to represent metadata in Relationship Objects, but now metadata support for relationships should be complete.

The difference is subtle. Here's metadata in the Relationship Object:

{
  "data": {
    "type": "people",
    "id": "88223",
    "attributes": {
      ...
    },
    "relationships": {
      "pet": {
        "data": {
          "type": "dogs",
          "id": "123"
        },
        "meta": {
          "nickname": "Sparks"
        }
      }
    }
  }
}

Here's metadata in the Resource Identifier Object (look for it one level deeper next to the id/type of the related resource):

{
  "data": {
    "type": "people",
    "id": "88223",
    "attributes": {
      ...
    },
    "relationships": {
      "pet": {
        "data": {
          "type": "dogs",
          "id": "123",
          "meta": {
            "nickname": "Sparks"
          }
        }
      }
    }
  }
}

In order to support this new metadata location, the following breaking change was made.

⚠️ Breaking Changes ⚠️ ToOneRelationship and ToManyRelationship both gained an additional generic type parameter named IdMetaType. This means that any code that uses them directly or typealiases them will need to be updated.

// before
ToOneRelationship<Identifiable: JSONAPI.JSONAPIIdentifiable, MetaType: JSONAPI.Meta, LinksType: JSONAPI.Links>

// after
ToOneRelationship<Identifiable: JSONAPI.JSONAPIIdentifiable, IdMetaType: JSONAPI.Meta, MetaType: JSONAPI.Meta, LinksType: JSONAPI.Links>

//before
ToManyRelationship<Relatable: JSONAPI.Relatable, MetaType: JSONAPI.Meta, LinksType: JSONAPI.Links>

//after
ToManyRelationship<Relatable: JSONAPI.Relatable, IdMetaType: JSONAPI.Meta, MetaType: JSONAPI.Meta, LinksType: JSONAPI.Links>

if you don't need to take advantage of this new metadata location, you can do away with it just like with any other option your relationship does not need. Here are two examples of typealiases that do away with all of the various auxiliary information:

typealias ToOne<R> = ToOneRelationship<R, NoIdMetadata, NoMetadata, NoLinks>
typealias ToMany<R> = ToManyRelationship<R, NoIdMetadata, NoMetadata, NoLinks>

If you do need to use the new metadata location, it is really just JSONAPI.Meta like any other metadata -- that is, it really just needs to be Codable and Equatable.

if you've got a resource with a relationship containing metadata inside the Resource Identifier Object, see the new documentation for information on getting at that metadata!

Version 4.0.0 Stable Release - 2020-05-31 23:04:40

Additions & Fixes

Release 4.0.0 has changed in the following ways since version 3.x. The following list includes changes for all 4.0.0 alpha and release candidates. There are no functional changes between the latest release candidate and this stable release.

ResourceObject is now Hashable when it is not Unidentified. ➕ ResourceObject now conforms to Swift.Identifiable when it is not Unidentified. ➕ CompoundResource and new Document initializers make document creation more ergonomic. ➕ Modify Document.SuccessDocument to guarantee non-optional properties where appropriate. ➕ SucceedableJSONAPIDocument and FailableJSONAPIDocument improve generic abstraction story. ➕ MetaRelationship added for relationships with only links and metadata. 🐞 Fix/Improve errors during decoding caused by malformed ResourceObject relationships.

Breaking Changes

⚠️ Bump minimum Swift version to 5.2 📛 Renames Identifiable to JSONAPIIdentifiable to avoid confusion and conflict with Swift.Identifiable. 📛 Renames JSONAPIIdentifiable's Identifier associated type to ID to match that of the Swift Identifiable type. 🗑 Remove deprecated subscript access to ResourceObject attributes.

Suggested approach to migrating code:

  • Find JSONAPI.Identifiable or Identifiable (careful not to pick up any uses of Swift Identifiable in your codebase) and replace with JSONAPIIdentifiable.
  • Anywhere you are using Identifier() in the context of a ResourceObject to construct its ID, replace with ID().
  • Anywhere you are using subscript access on ResourceObject to access an attribute, switch to using dynamic member access (i.e. widget[\.cog] changes to widget.cog`).

Bugfix for typealias/associatedtype conflict. - 2020-05-30 02:25:00

One protocol's typealias will collide with another protocol's associatedtype in very ugly ways. This is why for now we will have ResourceObject.ID and ResourceObject.Id (both equally valid ways of referring to a ResourceObject's Identifying type.

Bugfix for ResourceObjectProxy - 2020-05-29 22:45:41

Attempt to fix a bug that only shows up in edge cases where ResourceObjectProxy.ID is used directly and must be equal to ResourceObject.ID.

Adds Swift `Identifiable` conformance to `ResourceObject` - 2020-05-29 21:57:41

In order to reduce the burden of conflict on users of the library, JSONAPI.Identifiable has been renamed to JSONAPIIdentifiable and (for all platforms that support it) conformance to Swift.Identifiable has been added to ResourceObject.

⚠️ Breaking Changes ⚠️ Sorry to introduce a breaking change during the release candidates. Conformance to Swift.Identifiable is really beneficial, especially if you ever pull a model into SwiftUI.

  • Renamed Identifiable -> JSONAPIIdentifiable
  • Renamed JSONAPIIdentifiable's Identifier associated type to ID to match that of the Swift Identifiable type.
  • ~Removed ResourceObject.Id (i.e. consolidated so that the same type alias as is used by JSONAPIIdentifiable and Swift.Identifiable is just named ResourceObject.ID). Otherwise there is a confusing co-existence of both ResourceObject.ID and ResourceObject.Id.~
  • [EDIT] As of the soon-to-be-released 4.0.0-rc.3.2, I will have re-introduced ResourceObject.Id which means that ResourceObject.ID and ResourceObject.Id will be equally valid. Not crazy about it, but no easy and satisfactory way to deal with it before the release of 4.0.0.

Suggested approach to updating code:

  1. Anywhere you are using JSONAPI.Identifiable, find JSONAPI.Identifiable or Identifiable (careful not to pick up on Swift Identifiable in your codebase) and replace with JSONAPIIdentifiable.
  2. Anywhere you are using Identifier() ~or Id()~ in the context of a ResourceObject to construct its ID, replace with ID(). [EDIT] As of 4.0.0-rc.3.2 ResourceObject.ID and ResourceObject.Id will both be equally valid.

Add relatives filtering to CompoundResource - 2020-05-27 00:26:31

Adds filtering of related resources to CompoundResource.

4.0 Release Candidate 1 - 2020-05-26 20:24:16

Changes since Alpha 3:

  • Add CompoundResource
  • Add SucceedableJSONAPIDocument and FailableJSONAPIDocument protocols
  • Make ResourceObject Hashable when its Id is Hashable.
  • Make Id's Hashable conformance take its type into consideration since the Id type's primary motivation is to retain differentiation based on the type of thing being identified.

Additional abstraction points around resource body - 2020-05-19 02:22:14

Expose abstraction points around single and many resource bodies that allow generic extensions to access the primary resource value(s).

Adds a couple of convenience initializers on Document types. - 2020-05-17 04:59:57

Fix inability to represent Relationship Objects with no `data` property. - 2020-05-08 08:04:27

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

This is a niche issue, but now a MetaRelationship can be used in place of a ToOneRelationship or ToManyRelationship when a Relationship Object with only links and/or meta is needed.

First 4.0 Alpha Release - 2020-05-08 06:17:15

  • Bump minimum Swift version to 5.2
  • Remove deprecated subscript access to ResourceObject attributes.
  • Fix/Improve errors during decoding caused by malformed ResourceObject relationships.
  • Modify Document.SuccessDocument to guarantee non-optional properties where the fact that it is a success document is able to provide such guarantees. For example, .primaryResource and .includes are now non-optional on SuccessDocument.

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.