{"id":15030385,"url":"https://github.com/marksands/bettercodable","last_synced_at":"2025-05-15T09:07:51.975Z","repository":{"id":35209009,"uuid":"216141915","full_name":"marksands/BetterCodable","owner":"marksands","description":"Better Codable through Property Wrappers","archived":false,"fork":false,"pushed_at":"2023-11-23T14:20:03.000Z","size":59,"stargazers_count":1755,"open_issues_count":13,"forks_count":80,"subscribers_count":17,"default_branch":"master","last_synced_at":"2025-05-15T09:07:40.764Z","etag":null,"topics":["codable","property-wrappers","swift-package-manager","swift5-1"],"latest_commit_sha":null,"homepage":"http://marksands.github.io/2019/10/21/better-codable-through-property-wrappers.html","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/marksands.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null}},"created_at":"2019-10-19T03:12:48.000Z","updated_at":"2025-05-12T16:00:39.000Z","dependencies_parsed_at":"2024-01-02T23:54:35.381Z","dependency_job_id":null,"html_url":"https://github.com/marksands/BetterCodable","commit_stats":{"total_commits":40,"total_committers":16,"mean_commits":2.5,"dds":0.725,"last_synced_commit":"2b4a27f03c8fe80f195ac2b6ac1764213406ee89"},"previous_names":[],"tags_count":5,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/marksands%2FBetterCodable","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/marksands%2FBetterCodable/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/marksands%2FBetterCodable/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/marksands%2FBetterCodable/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/marksands","download_url":"https://codeload.github.com/marksands/BetterCodable/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":254310520,"owners_count":22049470,"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":["codable","property-wrappers","swift-package-manager","swift5-1"],"created_at":"2024-09-24T20:13:13.838Z","updated_at":"2025-05-15T09:07:46.963Z","avatar_url":"https://github.com/marksands.png","language":"Swift","readme":"# Better Codable through Property Wrappers\n\nLevel up your `Codable` structs through property wrappers. The goal of these property wrappers is to avoid implementing a custom `init(from decoder: Decoder) throws` and suffer through boilerplate.\n\n## @LossyArray\n\n`@LossyArray` decodes Arrays and filters invalid values if the Decoder is unable to decode the value. This is useful when the Array contains non-optional types and your API serves elements that are either null or fail to decode within the container.\n\n### Usage\n\nEasily filter nulls from primitive containers\n\n```Swift\nstruct Response: Codable {\n    @LossyArray var values: [Int]\n}\n\nlet json = #\"{ \"values\": [1, 2, null, 4, 5, null] }\"#.data(using: .utf8)!\nlet result = try JSONDecoder().decode(Response.self, from: json)\n\nprint(result) // [1, 2, 4, 5]\n```\n\nOr silently exclude failable entities\n\n```Swift\nstruct Failable: Codable {\n    let value: String\n}\n\nstruct Response: Codable {\n    @LossyArray var values: [Failable]\n}\n\nlet json = #\"{ \"values\": [{\"value\": 4}, {\"value\": \"fish\"}] }\"#.data(using: .utf8)!\nlet result = try JSONDecoder().decode(Response.self, from: json)\n\nprint(result) // [Failable(value: \"fish\")]\n```\n\n## @LossyDictionary\n\n`@LossyDictionary` decodes Dictionaries and filters invalid key-value pairs if the Decoder is unable to decode the value. This is useful if the Dictionary is intended to contain non-optional values and your API serves values that are either null or fail to decode within the container.\n\n### Usage\n\nEasily filter nulls from primitive containers\n\n```Swift\nstruct Response: Codable {\n    @LossyDictionary var values: [String: String]\n}\n\nlet json = #\"{ \"values\": {\"a\": \"A\", \"b\": \"B\", \"c\": null } }\"#.data(using: .utf8)!\nlet result = try JSONDecoder().decode(Response.self, from: json)\n\nprint(result) // [\"a\": \"A\", \"b\": \"B\"]\n```\n\nOr silently exclude failable entities\n\n```Swift\nstruct Failable: Codable {\n    let value: String\n}\n\nstruct Response: Codable {\n    @LossyDictionary var values: [String: Failable]\n}\n\nlet json = #\"{ \"values\": {\"a\": {\"value\": \"A\"}, \"b\": {\"value\": 2}} }\"#.data(using: .utf8)!\nlet result = try JSONDecoder().decode(Response.self, from: json)\n\nprint(result) // [\"a\": \"A\"]\n```\n\n## @DefaultCodable\n\n`@DefaultCodable` provides a generic property wrapper that allows for default values using a custom `DefaultCodableStrategy`. This allows one to implement their own default behavior for missing data and get the property wrapper behavior for free. Below are a few common default strategies, but they also serve as a template to implement a custom property wrapper to suit your specific use case.\n\nWhile not provided in the source code, it's a sinch to create your own default strategy for your custom data flow.\n\n```Swift\nstruct RefreshDaily: DefaultCodableStrategy {\n    static var defaultValue: CacheInterval { return CacheInterval.daily }\n}\n\nstruct Cache: Codable {\n    @DefaultCodable\u003cRefreshDaily\u003e var refreshInterval: CacheInterval\n}\n\nlet json = #\"{ \"refreshInterval\": null }\"#.data(using: .utf8)!\nlet result = try JSONDecoder().decode(Cache.self, from: json)\n\nprint(result) // Cache(refreshInterval: .daily)\n```\n\n## @DefaultFalse\n\nOptional Bools are weird. A type that once meant true or false, now has three possible states: `.some(true)`, `.some(false)`, or `.none`. And the `.none` condition _could_ indicate truthiness if BadDecisions™ were made.\n\n`@DefaultFalse` mitigates the confusion by defaulting decoded Bools to false if the Decoder is unable to decode the value, either when null is encountered or some unexpected type.\n\n### Usage\n\n```Swift\nstruct UserPrivilege: Codable {\n    @DefaultFalse var isAdmin: Bool\n}\n\nlet json = #\"{ \"isAdmin\": null }\"#.data(using: .utf8)!\nlet result = try JSONDecoder().decode(Response.self, from: json)\n\nprint(result) // UserPrivilege(isAdmin: false)\n```\n\n## @DefaultEmptyArray\n\nThe weirdness of Optional Booleans extends to other types, such as Arrays. Soroush has a [great blog post](http://khanlou.com/2016/10/emptiness/) explaining why you may want to avoid Optional Arrays. Unfortunately, this idea doesn't come for free in Swift out of the box. Being forced to implement a custom initializer in order to nil coalesce nil arrays to empty arrays is no fun.\n\n`@DefaultEmptyArray` decodes Arrays and returns an empty array instead of nil if the Decoder is unable to decode the container.\n\n### Usage\n\n```Swift\nstruct Response: Codable {\n    @DefaultEmptyArray var favorites: [Favorite]\n}\n\nlet json = #\"{ \"favorites\": null }\"#.data(using: .utf8)!\nlet result = try JSONDecoder().decode(Response.self, from: json)\n\nprint(result) // Response(favorites: [])\n```\n\n## @DefaultEmptyDictionary\n\nAs mentioned previously, Optional Dictionaries are yet another container where nil and emptiness collide.\n\n `@DefaultEmptyDictionary` decodes Dictionaries and returns an empty dictionary instead of nil if the Decoder is unable to decode the container.\n\n### Usage\n\n```Swift\nstruct Response: Codable {\n    @DefaultEmptyDictionary var scores: [String: Int]\n}\n\nlet json = #\"{ \"scores\": null }\"#.data(using: .utf8)!\nlet result = try JSONDecoder().decode(Response.self, from: json)\n\nprint(result) // Response(values: [:])\n```\n\n## @LosslessValue\n\nAll credit for this goes to [Ian Keen](https://twitter.com/iankay).\n\nSomtimes APIs can be unpredictable. They may treat some form of Identifiers or SKUs as `Int`s for one response and `String`s for another. Or you might find yourself encountering `\"true\"` when you expect a boolean. This is where `@LosslessValue` comes into play.\n\n`@LosslessValue` will attempt to decode a value into the type that you expect, preserving the data that would otherwise throw an exception or be lost altogether. \n\n### Usage\n\n```Swift\nstruct Response: Codable {\n    @LosslessValue var sku: String\n    @LosslessValue var isAvailable: Bool\n}\n\nlet json = #\"{ \"sku\": 12345, \"isAvailable\": \"true\" }\"#.data(using: .utf8)!\nlet result = try JSONDecoder().decode(Response.self, from: json)\n\nprint(result) // Response(sku: \"12355\", isAvailable: true)\n```\n\n## Date Wrappers\n\nOne common frustration with `Codable` is decoding entities that have mixed date formats. `JSONDecoder` comes built in with a handy `dateDecodingStrategy` property, but that uses the same date format for all dates that it will decode. And often, `JSONDecoder` lives elsewhere from the entity forcing tight coupling with the entities if you choose to use its date decoding strategy.\n\nProperty wrappers are a nice solution to the aforementioned issues. It allows tight binding of the date formatting strategy directly with the property of the entity, and allows the `JSONDecoder` to remain decoupled from the entities it decodes. The `@DateValue` wrapper is generic across a custom `DateValueCodableStrategy`. This allows anyone to implement their own date decoding strategy and get the property wrapper behavior for free. Below are a few common Date strategies, but they also serve as a template to implement a custom property wrapper to suit your specific date format needs.\n\nThe following property wrappers are heavily inspired by [Ian Keen](https://twitter.com/iankay).\n\n## ISO8601Strategy\n\n`ISO8601Strategy` relies on an `ISO8601DateFormatter` in order to decode `String` values into `Date`s. Encoding the date will encode the value into the original string value.\n\n### Usage\n\n```Swift\nstruct Response: Codable {\n    @DateValue\u003cISO8601Strategy\u003e var date: Date\n}\n\nlet json = #\"{ \"date\": \"1996-12-19T16:39:57-08:00\" }\"#.data(using: .utf8)!\nlet result = try JSONDecoder().decode(Response.self, from: json)\n\n// This produces a valid `Date` representing 39 minutes and 57 seconds after the 16th hour of December 19th, 1996 with an offset of -08:00 from UTC (Pacific Standard Time).\n```\n\n## RFC3339Strategy\n\n`RFC3339Strategy` decodes RFC 3339 date strings into `Date`s. Encoding the date will encode the value back into the original string value.\n\n### Usage\n\n```Swift\nstruct Response: Codable {\n    @DateValue\u003cRFC3339Strategy\u003e var date: Date\n}\n\nlet json = #\"{ \"date\": \"1996-12-19T16:39:57-08:00\" }\"#.data(using: .utf8)!\nlet result = try JSONDecoder().decode(Response.self, from: json)\n\n// This produces a valid `Date` representing 39 minutes and 57 seconds after the 16th hour of December 19th, 1996 with an offset of -08:00 from UTC (Pacific Standard Time).\n```\n\n## TimestampStrategy\n\n`TimestampStrategy` decodes `Double`s of a unix epoch into `Date`s. Encoding the date will encode the value into the original `TimeInterval` value.\n\n### Usage\n\n```Swift\nstruct Response: Codable {\n    @DateValue\u003cTimestampStrategy\u003e var date: Date\n}\n\nlet json = #\"{ \"date\": 978307200.0 }\"#.data(using: .utf8)!\nlet result = try JSONDecoder().decode(Response.self, from: json)\n\n// This produces a valid `Date` representing January 1st, 2001.\n```\n\n## YearMonthDayStrategy\n\n `@DateValue\u003cYearMonthDayStrategy\u003e` decodes string values into `Date`s using the date format `y-MM-dd`. Encoding the date will encode the value back into the original string format.\n\n### Usage\n\n```Swift\nstruct Response: Codable {\n    @DateValue\u003cYearMonthDayStrategy\u003e var date: Date\n}\n\nlet json = #\"{ \"date\": \"2001-01-01\" }\"#.data(using: .utf8)!\nlet result = try JSONDecoder().decode(Response.self, from: json)\n\n// This produces a valid `Date` representing January 1st, 2001.\n```\n\nOr lastly, you can mix and match date wrappers as needed where the benefits truly shine\n\n```Swift\nstruct Response: Codable {\n    @DateValue\u003cISO8601Strategy\u003e var updatedAt: Date\n    @DateValue\u003cYearMonthDayStrategy\u003e var birthday: Date\n}\n\nlet json = #\"{ \"updatedAt\": \"2019-10-19T16:14:32-05:00\", \"birthday\": \"1984-01-22\" }\"#.data(using: .utf8)!\nlet result = try JSONDecoder().decode(Response.self, from: json)\n\n// This produces two valid `Date` values, `updatedAt` representing October 19, 2019 and `birthday` January 22nd, 1984.\n```\n\n## Installation\n\n### CocoaPods\n\n```ruby\npod 'BetterCodable', '~\u003e 0.1.0'\n```\n\n### Swift Package Manager\n\n## Attribution\n\nThis project is licensed under MIT. If you find these useful, please tell your boss where you found them.\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmarksands%2Fbettercodable","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmarksands%2Fbettercodable","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmarksands%2Fbettercodable/lists"}