{"id":13990706,"url":"https://github.com/iZettle/Lift","last_synced_at":"2025-07-22T13:31:00.812Z","repository":{"id":27023780,"uuid":"81565074","full_name":"iZettle/Lift","owner":"iZettle","description":"Lift is a Swift library for generating and extracting values into and out of JSON-like data structures.","archived":true,"fork":false,"pushed_at":"2022-09-30T11:06:46.000Z","size":14549,"stargazers_count":34,"open_issues_count":5,"forks_count":5,"subscribers_count":24,"default_branch":"master","last_synced_at":"2024-10-30T17:14:49.640Z","etag":null,"topics":["ios","json","mac","swift","tvos","watchos"],"latest_commit_sha":null,"homepage":null,"language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/iZettle.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE.md","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":".github/CODEOWNERS","security":null,"support":null}},"created_at":"2017-02-10T12:53:13.000Z","updated_at":"2024-10-30T12:56:06.000Z","dependencies_parsed_at":"2022-07-27T09:02:46.056Z","dependency_job_id":null,"html_url":"https://github.com/iZettle/Lift","commit_stats":null,"previous_names":[],"tags_count":14,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/iZettle%2FLift","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/iZettle%2FLift/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/iZettle%2FLift/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/iZettle%2FLift/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/iZettle","download_url":"https://codeload.github.com/iZettle/Lift/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":227098939,"owners_count":17730675,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["ios","json","mac","swift","tvos","watchos"],"created_at":"2024-08-09T13:03:08.113Z","updated_at":"2024-11-29T10:31:24.942Z","avatar_url":"https://github.com/iZettle.png","language":"Swift","funding_links":[],"categories":["Swift"],"sub_categories":[],"readme":"\u003cimg src=\"https://github.com/iZettle/Lift/blob/master/lift-logo.png?raw=true\" height=\"140px\" /\u003e\n\n[![Build Status](https://travis-ci.org/iZettle/Lift.svg?branch=master)](https://travis-ci.org/iZettle/Lift)\n[![Platforms](https://img.shields.io/badge/platform-%20iOS%20|%20macOS%20|%20tvOS%20|%20linux-gray.svg)](https://img.shields.io/badge/platform-%20iOS%20|%20macOS%20|%20tvOS%20|%20linux-gray.svg)\n[![Carthage Compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage)\n[![Swift Package Manager Compatible](https://img.shields.io/badge/SwiftPM-Compatible-brightgreen.svg)](https://github.com/apple/swift-package-manager)\n![Xcode version](https://img.shields.io/badge/Xcode-13.0.0-blue)\n\nLift is a Swift library for generating and extracting values into and out of JSON-like data structures. Lift was carefully designed to meet the following requirements:\n\n- Use easy and intuitive syntax using subscripting.\n- Be extendable for use with your custom types.\n- Support of retroactive modeling/conformance.\n- Do not enforce how to structure your data models.\n- Be type safe and explicit about errors.\n- Work with any key value structured data such as p-lists and user defaults.\n- Provide detailed errors and support custom validation.\n- Use value semantics for the `Jar` container.\n\n### Example usage\n\nLift is simple, yet powerful. Let us see how to use it with a custom type:\n\n```swift\nstruct User {\n  let name: String\n  let age: Int\n}\n```\n\nJust conform to `JarElement` to let Lift know how to transform your type:\n\n```swift\nextension User: JarElement {\n  init(jar: Jar) throws {\n    name = try jar[\"name\"]^\n    age = try jar[\"age\"]^\n  }\n \n  var jar: Jar {\n    return [\"name\": name, \"age\": age]\n  }\n}\n```\n\nThen given some JSON, you can now construct a `Jar` and extract users from it using the lift operator `^`:\n\n```swift\nlet json = \"[{\\\"name\\\": \\\"Adam\\\", \\\"age\\\": 25}, {\\\"name\\\": \\\"Eve\\\", \\\"age\\\": 20}]\"\nlet jar = try Jar(json: json)\nvar users: [User] = try jar^\n```\n\nAnd it is as easy to move your model values back to JSON:\n\n```swift\nusers.append(User(name: \"Junior\", age: 2))\nlet newJson = try String(json: Jar(users), prettyPrinted: true)\n```\n\nLift will even work with other JSON-like structured data such as p-lists and `UserDefaults`:\n\n```swift\nlet users: [User] = try UserDefaults.standard[\"users\"]^\n```\n\nCheck the [Usage](#usage) section for more information and examples.\n\n### Contents:\n\n- [Requirements](#requirements)\n- [Installation](#installation)\n- [Note on `Codable`](#note-on-codable)\n- [Usage](#usage)\n\t- [Introduction](#introduction)\n\t- [JSON Serialization](#json-serialization)\n\t- [Generating JSON](#generating-json)\n\t- [Modifying JSON](#modifiying-json)\n\t- [Arrays](#arrays)\n\t- [Missing values](#missing-values)\n\t- [Heterogenuos values](#heterogenuos-values)\n\t- [Transformation of values](#transformation-of-values)\n\t- [Beyond JSON](#beyond-json)\n\t- [Handling custom types](#handling-custom-types)\n\t- [Model structure](#model-structure)\n\t- [Handling errors](#handling-errors)\n- [Field tested](#field-tested)\n- [Learn more](#learn-more)\n- [Collaborate](#collaborate)\n\n## Requirements\n\n- Xcode `9.3+`\n- Swift 4.1\n- Platforms:\n  * iOS `9.0+`\n  * macOS `10.11+`\n  * tvOS `9.0+`\n  * watchOS `2.0+`\n  * Linux\n\n\n## Installation\n\n#### [Carthage](https://github.com/Carthage/Carthage)\n\n```shell\ngithub \"iZettle/Lift\" \u003e= 2.3\n```\n\n#### [Cocoa Pods](https://github.com/CocoaPods/CocoaPods)\n\n```ruby\nplatform :ios, '9.0'\nuse_frameworks!\n\ntarget 'Your App Target' do\n  pod 'Lift', '~\u003e 2.3'\nend\n```\n\n#### [Swift Package Manager](https://github.com/apple/swift-package-manager)\n\n```swift\nimport PackageDescription\n\nlet package = Package(\n  name: \"Your Package Name\",\n  dependencies: [\n      .Package(url: \"https://github.com/iZettle/Lift.git\",\n               majorVersion: 2)\n  ]\n)\n```\n\n## Note on `Codable`\n\nSwift 4 introduced `Codable` with the \"promise\" to have solved working with JSON once and for all. And yes, many of the examples shown are just close to magic. But when your models start to diverge from the simple ones to ones mapping between model and JSON, the magic seems to go away. Now you are back to implementing everything yourself and this using a quite verbose API. The current version of Swift also lacks APIs for building and parsing JSON on the fly (not going through model objects) which is common when e.g. building and parsing network requests. Hence, we believe the demand for third party JSON libraries will still be there for some time to come.\n\n## Usage\n\n### Introduction\n\nLet us start out with a simple example of how to extract data from some key value structured data such as JSON:\n\n```swift\nlet jar: Jar = [\"name\": \"Lift\", \"version\": 1.0]\nlet name: String = try jar[\"name\"]^\nlet version: Double = try jar[\"version\"]^\n```\n\n`Jar` is Lift's container of heterogenous values. In this example it holds a dictionary. The operator `^` (called the lift operator) is used to extract values out of the jar container. Because the jar typically holds values that are not known at compile time, extracting them might fail. This might happen if the value is missing, if the value is not of the expected type, or if some other validation is failing. This is why you will always see a `try` in the presence of the lift operator `^`.\n\nAs mentioned, JSON does not always come in the form of a dictionary (key-values), but could also be simple primitive types or arrays of other JSON objects:\n\n```swift\nlet i: Int = try Jar(1)^\nlet b: Bool = try Jar(true)^\nlet a: [Int] = try Jar([1, 2, 3])^\nlet jar: Jar = [\"value\": \"lift\"]\nlet s: String = try jar[\"value\"]^\n```\n\nThe `^` operator is overloaded to allow conforming types to either be extracted as the type itself or as an optional version of it. You can also extract an array or optional array of conforming types:\n\n```swift\nlet i: Int = try jar^\nlet i: Int? = try jar^\nlet i: [Int] = try jar^\nlet i: [Int]? = try jar^\n```\n\n`Jar` implements subscripting for keys and indices and also allows them to be nested:\n\n```swift\nlet date: Date = try jar[\"payments\"][3][\"date\"]^\njar[\"payments\"][2][\"date\"] = Date()\n```\n\n\n### JSON serialization\n\nLift adds convenience initializers to construct a `Jar` from JSON and back:\n\n```swift\nlet json = \"{ \\\"val\\\": 3, \\\"vals\\\": [ 1, 2 ] }\"\nlet jar = try Jar(json: json)\nlet jsonString = try String(json: jar, prettyPrinted: true)\nlet jsonData = try Data(json: jar, prettyPrinted: false)\n```\n\nYou could also handle the serialization yourself and just pass an `Any` value:\n\n```swift\nlet json: Any = ...\nlet jar = try Jar(checked: json) // Will validate when constructed - slower\nlet jar = Jar(unchecked: json) // Will lazily validate at access - faster\n\nlet any: Any = try jar.asAny()\n```\n\n### Generating JSON\n\nTo help creating JSON, `Jar` implements several _expressible by literal_ protocols so you can write code like:\n\n```swift\nfunc send(_ jar: Jar) { ... }\n\nsend(true)\nsend(1)\nsend(3.14)\nsend(\"Hello\")\nsend([\"val\": 5])\nsend([1, 2, 3])\n```\n\nWhen `Jar` can't be inferred by the complier, you can explicitly specify the type:\n\n```swift\nlet jar: Jar = [\"val\": 5]\nlet jar: Jar = [1, 2, 3]\n```\n\nYou can also build nested hierarchies:\n\n```swift\nlet jar: Jar = [5, [\"val\": [1, 2]]]\n```\n\nAnd of course you can also build JSON from your custom types:\n\n```swift\nlet jar: Jar = [\"payment\": payment, \"date\": date]\n```\n\n### Modifying JSON\n\nBecause `Jar` is a value type with value semantics, you can modify your `Jar` value when declared as `var`.\n\n```swift\nvar jar = Jar()\njar[\"payment\"] = payment\njar[\"date\"] = Date()\n```\n\n```swift\nvar jar = Jar()\njar.append(payment)\njar.append(Date())\n```\n\nAnd if you need to modify your JSON before passing it on just make a copy:\n\n```swift\nfunc receive(jar: Jar) {\n  var jar = jar\n  jar[\"timeReceived\"] = Date()\n  // ...\n}\n```\n\nA `Jar` is never bound to a specific type or value, hence it is always ok to change it:\n\n```swift\nvar jar: Jar = true // jar holds a boolean\njar = [1, 2] // holds an array\njar[\"val\"] = 1 // holds a dictionary\njar = 4711 // holds an integer\njar = [\"val2\": 2] // holds a dictionary\njar = \"Hello\" // holds a string\n```\n\n\n### Arrays\n\nLift supports working with arrays of primitive types:\n\n```swift\nvar jar: Jar = [1, 2, 3]\n\njar[1] = 4\njar.append(5)\n\nlet val: Int = try jar[2]^\n```\n\nAs well as arrays of your custom types:\n\n```swift\nvar jar: Jar = [Payment(...), Payment(...), ...]\n\nlet payments: [Payment] = try jar^\njar[2] = Payment(...)\n```\n\n\n### Missing values and JSON null\n\nSometimes the existence of a value is a requirement, sometimes it is optional.\n\n```swift\nlet i: Int = try jar[\"val\"]^ // Will throw if val is missing or not an Int\nlet i: Int? = try jar[\"val\"]^ // Will return nil if val is missing or null, else throw if not an Int\nlet i: Int = try jar[\"val\"]^ ?? 4711 // Will throw if val is present and is not an Int\n```\n\nFor your convenience Lift treats a value set to JSON null the same as a missing value. But if you need to check for the presence of the actual null value itself you can write:\n\n```swift\nif let _: Null = try? jar[\"val\"]^ {\n  //...\n}\n```\n\nWhen building your JSON it is quite common that some values are optional:\n\n```swift\nlet optional: Int? = nil\nvar jar: Jar = [\"val\": 1]\njar[\"optional\"] = optional // -\u003e {\"val\": 1}\n```\n\nIt is also possible to add your optional inline:\n\n```swift\nvar jar: Jar = [\"val\": 1, \"optional\": optional] // -\u003e {\"val\": 1}\n```\n\nIf you actually want a JSON null value you can use the constant `null`:\n\n```swift\nvar jar: Jar = [\"val\": 1, \"optional\": optional ?? null] // -\u003e {\"val\": 1, \"optional\": null}\n```\n\n### Heterogenous values\n\nSometimes parts of your JSON could hold the union of different kinds of valid types. Then you could test between the different variations you support:\n\n```swift\nlet any: Any? = NSJSONSerialization...\nlet jar = try Jar(any) // any could be a dictionary, array or a primitive type\n\nif let int: Int = try? jar^ {\n  // ...\n} else if let ints: [Int] = try? jar^ {\n  // ...\n} else if let int: Int = try? jar[\"value\"]^ {\n  // ...\n}\n```\n\nJSON also supports arrays of mixed types:\n\n```swift\nlet jar = try Jar(any) // [ 1, [1, 2], { \"val\" : 3 } ] -- [Int, Array, Dictionary]\n\nlet int: Int = try jar[0]^\nlet array: [Int] = try jar[1]^\nlet dict: Jar = try jar[2]^\n```\n\n### Transformation of values\n\nYou sometimes need to transform the values extracted from a `Jar` before using them. This might happen when you are working with types that cannot conform to `JarRepresentable`, such as when using tuples:\n\n```swift\ntypealias User = (name: String, age: Int)\nlet users: [User] = try (jar^ as [Jar]).map { jar in \n  try (jar[\"name\"]^, jar[\"age\"]^) \n}\n```\n\nOr when your type does not conform to `JarRepresentable`, as it might need some additional initialization data:\n\n```swift\nlet account: Account = ...\n\nlet payments: [Payment] = try (jar[\"payments\"]^ as Array).map {\n  Payment(jar: $0, account: account)\n}\n```\n\nEven though you can manually transform values to add additional initialization data, it is often more convenient to add this data to the jar's context instead. Jar contexts will be described further down.\n\n### Beyond JSON\n\nSetting and getting values of unknown types is not unique to JSON. Many Cocoa APIs use dictionaries and many of them are based on similar principles as JSON, such as p-lists. Lift provides protocols for extending those types to grant them  access the power of Lift. E.g. Lift already extends UserDefaults:\n\n```swift\n// extension UserDefaults: MutatingValueForKey { }\n\nlet userDefaults = UserDefaults.standard\n\nlet date: Date? = try userDefaults[\"lastLaunched\"]^\nuserDefaults[\"lastLaunched\"] = Date()\n\nlet payments: [Payments] = try userDefaults[\"payments\"]^ ?? []\n```\n\n### JarConvertible \u0026 JarRepresentable\n\nOut of the box, the Lift library supports JSON dictionaries, arrays and its primitive types: string, number, bool and null. But it is easy to extend your own types to work with the Lift library as well.\n\nTo be able extract values out of a `Jar` using the lift operator `^`, you conform your type to the `JarConvertible` protocol:\n\n```swift\nprotocol JarConvertible {\n  init(jar: Jar) throws\n}\n```\n\nAnd to be able to convert your type to a `Jar`, you conform your type to `JarRepresentable`:\n\n```swift\nprotocol JarRepresentable {\n  var jar: Jar { get }\n}\n```\n\nIt is common to implement both these protocols hence the convenience `JarElement` type alias:\n\n```swift\ntypealias JarElement = JarConvertible \u0026 JarRepresentable\n```\n\nThe Lift library includes extensions for the most common primitive types such as `Int`, `Bool`, `String`, etc., by conforming them to `JarElement`.\n\n\n### Handling custom types\n\nYour custom types are typically either simple types such as:\n\n```swift\nstruct Money {\n  let fractionized: Int\n}\n\nextension Money: JarElement {\n  init(jar: Jar) throws {\n    fractionized = try jar^\n  }\n\n  var jar: Jar { return Jar(fractionized) }\n}\n\nlet jar: Jar = [\"amount\": Money(fractionized: 2000)]\nlet amount: Money = try jar[\"amount\"]^\n```\n\nOr perhaps more common, more complex and record like types such as:\n\n```swift\nstruct Payment {\n  let amount: Money\n  let date: Date\n}\n\nextension Payment: JarElement {\n  init(jar: Jar) throws {\n    amount = try jar[\"amount\"]^\n    date = try jar[\"date\"]^\n  }\n\n  var jar: Jar {\n    return [\"amount\": amount, \"date\": date]\n  }\n}\n\nlet jar: Jar = [\"payment\": Payment(...)]\nlet payment: Payment = try jar[\"payment\"]^\n```\n\nTo make it easier to conform your custom enums with raw values, Lift comes with some default implementations. All you have to do is to conform the enum to `JarElement` to be able to use it with `Jar`s:\n\n```swift\nenum MyEnum: String, JarElement {\n  case one, two, three\n}\n\nlet jar: Jar = [\"enum\": MyEnum.two]\nlet str: String = try jar[\"enum\"]^ // -\u003e \"two\"\nlet myEnum: MyEnum = try jar[\"enum\"]^ // -\u003e .two\n```\n\n`JarConvertible` requires you to implement a required init. This can be problematic if you work with non-final classes where you cannot update the source itself, such as when the class originates from Objective-C or another external source. In those cases you have to use the `Liftable` protocol instead:\n\n```swift\nextension MyClass: Liftable {\n  static func lift(from jar: Jar) throws -\u003e MyClass {\n    // Implementation\n  }\n}\n```\n\n### Model structure\n\nThe Lift library does not enforce the structure of you custom types and also allows retroactive modeling. It is up to you how you decide to map between your types and JSON. For example you might have enums with associative values (in this example a recursive one):\n\n```swift\n// [ { \"type\": \"Product\", \"uuid\": ”3b0bb980-2c…” },\n//   { \"type\": \"Folder\", \"name\": \"Coffee\", \"items\": [\n//        { \"type\": \"Product\", \"uuid\": ”3e493140-2c…” },\n//        { \"type\": \"Product\", ”uuid\": ”3e623780-2c…” }] },\n//   ... ]\n\nindirect enum FlowLayout {\n  case product(uuid: UUID)\n  case folder(name: String, items: [FlowLayout])\n}\n```\n\nBecause the JSON format has a weaker type-system than Swift, stricter validation becomes more important:\n\n```swift\nextension FlowLayout: JarConvertible  {\n  init(jar: Jar) throws {\n    switch try jar[\"type\"]^ as String {\n    case \"Product\":\n      self = try .product(uuid: jar[\"uuid\"]^)\n    case \"Folder\":\n      self = try .folder(name: jar[\"name\"]^, items: jar[\"items\"]^)\n    case let type:\n      throw jar.assertionFailure(\"Unknown layout type: \\(type)\")\n    }\n  }\n}\n```\n\nEven for these more complex types, generation of JSON is still quite straightforward:\n\n```swift\nextension FlowLayout: JarRepresentable {\n  var jar: Jar {\n      switch self {\n      case let .product(uuid):\n        return [\"type\": \"Product\", \"uuid\": uuid]\n      case let .folder(name, items):\n        return [\"type\": \"Folder\", \"name\": name, \"items\": items]\n    }\n  }\n}\n```\n\n### Handling errors\n\nBecause JSON is typically nested, it is useful to extend errors with some positioning and context. Lift tries to keep track of the closest context and \"key-path\" into your data and will expose those in `LiftError`s:\n\n```swift\nstruct LiftError: Error {\n  let message: String\n  let key: String\n  let context: String\n}\n```\n\nBecause the context and key-path are really valuable during debugging, it is important to not lose those when throwing validation errors. Hence, Lift has added special assert helper methods to `Jar` that you are encouraged to use:\n\n```swift\ninit(jar: Jar) throws {\n  // ...\n  try jar.assert(i \u003e 0, \"Must greater than zero\")\n\n  guard validate(...) else {\n    throw jar.assertionFailure(\"Not a business nor a person\")\n  }\n  \n  url = try jar.assertNotNil(URL(string: jar^), \"Invalid URL\")\n  // ...\n}\n```\n\n### Jar context\n\nSometimes your type's initializer needs access to more data than what is included in the JSON itself. E.g. perhaps your `Money` type needs a currency as well, but your JSON does not provide that or provides it far away from the actual amount value itself. This is where you can pass the currency in the jar's context instead:\n\n```swift\nstruct Money {\n  let fractionized: Int\n  let currency: Currency\n}\n\nextension Money: JarElement {\n  init(jar: Jar) throws {\n    fractionized = try jar^\n    currency = try jar.context.get() // will extract the currency\n  }\n\n  var jar: Jar { return Jar(fractionized) }\n}\n\nlet amount: Money = try jar.union(context: currency)[\"amount\"]^\n```\n\nThe jar's context is also useful for customizing the encoding and decoding of your types. E.g. `Date` will by default use the ISO8601 date format, but by providing another `DateFormatter` in the jar's context you could customize the date format:\n\n```swift\nextension Date: JarConvertible, JarRepresentableWithContext {\n  init(jar: Jar) throws {\n    let formatter: DateFormatter = jar.context.get() ?? .iso8601\n    self = try jar.assertNotNil(formatter.date(from: jar^), \"Date failed to convert using formatter with dateFormat: \\(formatter.dateFormat)\")\n  }\n\n  func asJar(using context: Jar.Context) -\u003e Jar {\n    let formatter: DateFormatter = context.get() ?? .iso8601\n    return Jar(formatter.string(from: self))\n  }\n}\n```\n\nAs `JarRepresentable` does not provide any context, you will instead conform to `JarRepresentableWithContext` that passes the context in `asJar`:\n\n```swift\nprotocol JarRepresentableWithContext: JarRepresentable {\n  func asJar(using context: Jar.Context) -\u003e Jar\n}\n```\n\nThe context could either be set externally or as part of some other type's encoding/decoding such as:\n\n```swift\nstruct Payment {\n  let amount: Money\n  let date: Date\n}\n\nextension Payment: JarElement {\n  init(jar: Jar) throws {\n    let jar = jar.union(context: DateFormatter.custom)\n    amount = try jar[\"amount\"]^ // a currency must be provided in the jar's context\n    date = try jar[\"date\"]^ // date will format using DateFormatter.custom\n  }\n\n  var jar: Jar {\n    let jar: Jar = [\"amount\": amount, \"date\": date]\n    return jar.union(context: DateFormatter.custom)\n  }\n}\n\nlet payment: Payment = try jar.union(context: currency)[\"payment\"]^\n```\n\n## Field tested\n\nLift was developed, evolved and field-tested over the course of several years, and is pervasively used in [iZettle](https://izettle.com)'s highly acclaimed point of sales app for communicating with iZettle's comprehensive set of backend services.\n\n## Collaborate\n\nYou can collaborate with us on our Slack workspace. Ask questions, share ideas or maybe just participate in ongoing discussions. To get an invitation, write to us at [ios-oss@izettle.com](mailto:ios-oss@izettle.com)\n\n## Learn more\n\nTo learn more about how Lift's APIs turned the way they did, we recommend reading the article:\n\n- [API Design - Deriving Lift](https://medium.com/izettle-engineering/deriving-lift-d83f8b6d0b38)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FiZettle%2FLift","html_url":"https://awesome.ecosyste.ms/projects/github.com%2FiZettle%2FLift","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FiZettle%2FLift/lists"}