GithubHelp home page GithubHelp logo

marksands / bettercodable Goto Github PK

View Code? Open in Web Editor NEW
1.7K 18.0 79.0 59 KB

Better Codable through Property Wrappers

Home Page: http://marksands.github.io/2019/10/21/better-codable-through-property-wrappers.html

License: MIT License

Swift 98.92% Ruby 1.08%
property-wrappers codable swift5-1 swift-package-manager

bettercodable's People

Contributors

abonham avatar igorkulman avatar kinoroy avatar lickel avatar marksands avatar moyerr avatar nachosoto avatar nirma avatar oliverkrakora avatar rnapier avatar serjooo avatar stevensorial avatar tapi avatar yonaskolb avatar zeveisenberg avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

bettercodable's Issues

Uncodable property wrapper

Hello,
please add non-codable property wrapper, as below, for properties that do not support codable protocol or do not want to decode / encode but can still be used and set separately.

/// Marks a property as non-codable and to be ignored
@propertyWrapper
struct Uncodable<Value> {
    
    private var value: Value?
    
    public init(wrappedValue: Value?) {
        self.value = wrappedValue
    }
    
    public var wrappedValue: Value? {
        get { value }
        set { self.value = newValue }
    }
}

extension Uncodable: Codable {
    
    public func encode(to encoder: Encoder) throws {
        // Skip encoding the wrapped value.
    }
    
    public init(from decoder: Decoder) throws {
        // The wrapped value is simply initialised to nil when decoded.
        self.value = nil
    }
}

thanks, grat project!

Purpose of LossyDecodableValue in LossyArray

I'm curious what the purpose of the LossyDecodableValue private struct is in the LossyArray wrapper. Since the generic T type is constrained to be a Codable type, couldn't the decoding in the wrapper be changed to:

let value = try container.decode(T.self)

I was playing around with this and it works as expected. Not sure I'm missing something and would appreciate some insight. Thanks!

Default Empty Lossy Array

Currently, you have to choose between:

  • Decoder not containing the array's key => @DefaultEmptyArray
  • Decoder contains the array's key, but one of the objects cannot be decoded => @LossyArray

But there is no possibility to have both behaviors.

For example:

Model:

struct User: Codable {
    var name: String
    var boughtArticles: [Article]
}

struct Article: Codable {
    var price: Int
}

Response JSON possibilities

{ 
  "name": "abc",
  "bought_articles": [
     { "price": 15 },
     { "proice": 10 } // Miss-typed key
  ]
}
  1. Because API is doing omitempty
{
  "name": "abc"
}

In this case, neither possibilities work properly.

  • @LossyArray would fail with the 2nd JSON
  • @DefaultEmptyArray would not fail, but it would be an empty array, and not contain the Article with price 10, that could've been correctly decoded.

What are your thoughts on this? How should it be handled?

I can create a @DefaultEmptyLossyArray, if there is no better option.

Update BetterCodable Version

Hey Mark! I just tried to use BetterCodable on a package I'm working on and realized that adding the dependency this way .package(url: "https://github.com/marksands/BetterCodable.git", from: "0.0.0") is picking 0.0.1 without the latest changes. This is because the release tag points to an earlier commit. I think we might need to do another release to have everyone use the latest changes.

Composing `LossyArray` with `DefaultEmptyArray`

Hi

I have a need to be able to handle both

  1. lossy array (some items cant be decoded)
  2. default empty (for when the property itself is not present).

Issue

I tried to compose those with something like:

  @DefaultEmptyArray
  @LossyArray
  var items: [Item]

but I get the swift compiler error

Property type '[Model.Item]' does not match that of the 'wrappedValue' property of its wrapper type 'DefaultEmptyArray'

Cause

This seems because one of the two types arent able to wrap the other (both want to take the original T).

Proposed Fix

To fix this I have done the following:

struct DefaultLossyArray<T: Codable>: DefaultCodableStrategy {
  static var defaultValue: LossyArray<T> { .init(wrappedValue: []) }
}

typealias EmptyLossyArray<T> = DefaultCodable<DefaultLossyArray<T>> where T: Codable

with this addition I can hadle the above two cases well.

  @EmptyLossyArray
  @LossyArray
  var items: [Item]

Pull Request

Do you feel something like this is worth adding to BetterCodable?

Please let me know, I can send a PR w/tests etc.

Thanks!

Addition of @KeyContainer for encoding/decoding dictionaries

Ole Begemann came up with a nice use case for fixing a shortcoming of Codable when dealing with Dictionaries that have a String based Enum key. Currently, these unexpectedly encode/decode as arrays rather than dictionary representations

https://gist.github.com/ole/5c31ca9e2919c815029784ef3b8fdc0d

If this is too specific of a use case, it might not make sense to include it, but I believe it solves a real problem that I have faced with Codable. What do you think about including this or a similar implementation? If you think it's useful, this package feels like a good home for it.

Rename RawValue to DefaultValue in DefaultCodingStrategy

With the current naming the DefaultCodingStrategy can't be used with enums and types conforming to RawRepresentable. RawValue is an associated type that is used by the RawRepresentable protocol. Therefore if the DefaultCodingStrategy is implemented by an enum the defaultValue refers to the RawValue of that enum type which does not work with the DefaultCodable property wrapper since the types of wrappedValue and defaultValue have to match.

Here a little example:

enum VehicleType: String {
case car
case motorcycle
case unknown
}

extension VehicleType: DefaultCodableStrategy {
  static var defaultValue: String { // the compiler suggests this type
      return Vehicle.unknown
  }
}

struct Vehicle: Codable {
 let name: String
 @DefaultCodable<VehicleType>
 let vehicleType: VehicleType // compiler error
}

Separate Decodable and Encodable?

Question

What are the options and thoughts on adding separate Decodable / Encodable support?

Problem

With the current implementation, everything relies on T: Codable.

So for example, the following code will not compile, because @DefaultEmptyArray requires ResponseObject to confirm to Encodable as well.

struct NetworkResponse: Decodable {
     @DefaultEmptyArray var someArray: [ResponseObject]
}

struct ResponseObject: Decodable {
    var age: Int
}

But I only care that NetworkResponse be Decodable, I don't need Encodable as well.

In order to make it compile. I need to conform/implement Encodable on ResponseObject.
It's trivial of course for the current ResponseObject, but when the object is more complex and uses a custom decoder initializer, I'm left with 2 bad options:

  • Spend time implementing the correct Encodable implementation, which will never be used
  • Leave a black/throwing implementation of the encode(into:) method, but then I lose compile time guarantee (which is the whole reason I'm using BetterCodable, so I don't have to write a bunch of useless guards)

Solutions

  • Ideal solution would be that DefaultCodable<Strategy<T>> would only require T to confirm to what the parent requires. In this case NetworkResponse only requires Decodable, so ResponseObject should only need Decodable.
    • However, I'm fairly certain there's no way to get parent types of a type (since ResponseObject could be used in 2 places, one requiring Decodable, the other one requiring Encodable)
  • The next solution would be to add 2 extra types for each property wrapper: DefaultDecodableEmptyArray etc. etc.
  • I don't know if there's anything else :\

@DefaultFalse cannot deal string number

let data = """
    {"a": "1"}
    """.data(using: .utf8)!

struct A: Decodable {
    @DefaultFalse var a: Bool
}

let m = try JSONDecoder.init().decode(A.self, from: data)

print(m.a)  // print is false

but actual true is right answer

Any ideas why I keep getting this error with cocoapods?

I have had this work once with the same project no issues. Then suddenly a pod install causes it to break. For whatever reason 0.1.0 works fine. Is this because the podspec is still saying s.version = '0.1.0'?

Our dep target is set to 12.4.

[!] CocoaPods could not find compatible versions for pod "BetterCodable":
  In snapshot (Podfile.lock):
    BetterCodable (= 0.1.0)

  In Podfile:
    ViewModels/Tests (from `Frameworks/ViewModels/`) was resolved to 0.1.0, which depends on
      BetterCodable (~> 0.2.0)

Specs satisfying the `BetterCodable (= 0.1.0), BetterCodable (~> 0.2.0)` dependency were found, but they required a higher minimum deployment target.

@DefaultFalse when key is missing

I have an API which either sends {"bool":true} or no key at all. But I still like to have a non-optional Bool in my Codable class because I have to access it from Objective-C (which can't access optional Bools). Is that possible somehow? Thanks.

Feature: Decode logical `Bool` into a real `Bool`

I've added a Bool type auto-conversion function, so if people want to add it to their projects, I'll try pulling requests.

JSON Swift
"false", "no", "0", "n", "f", 0, false false
"true", "yes", "1", "y", "t", 1, true true
import Foundation

@propertyWrapper
public struct BoolValue: Codable {
    public var wrappedValue: Bool

    public init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        if let stringifiedValue = try? container.decode(String.self) {
            switch stringifiedValue.lowercased() {
            case "false", "no", "0", "n", "f": wrappedValue = false
            case "true", "yes", "1", "y", "t": wrappedValue = true
            default:
                let description = "Expected to decode Bool but found a '\(stringifiedValue)' instead."
                throw DecodingError.dataCorruptedError(in: container, debugDescription: description)
            }
        } else if let integerifiedValue = try? container.decode(Int.self) {
            switch integerifiedValue {
            case 0: wrappedValue = false
            case 1: wrappedValue = true
            default:
                let description = "Expected to decode Bool but found a '\(integerifiedValue)' instead."
                throw DecodingError.dataCorruptedError(in: container, debugDescription: description)
            }
        } else {
            wrappedValue = try container.decode(Bool.self)
        }
    }

    public func encode(to encoder: Encoder) throws {
        try wrappedValue.encode(to: encoder)
    }
}

Usage:

@BoolValue var isFollow: Bool

protocol CodableEnumeration for enum

Thank you for your code ๐Ÿ‘ใ€‚
The following code also works on my project,
If you think it's useful, this package feels like a good home for it ๐Ÿคใ€‚

protocol CodableEnumeration: RawRepresentable, Codable where RawValue: Codable {
    static var defaultCase: Self { get }
}

extension CodableEnumeration {
    public init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        do {
            let decoded = try container.decode(RawValue.self)
            self = Self.init(rawValue: decoded) ?? Self.defaultCase
        } catch {
            self = Self.defaultCase
        }
    }
    public func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()
        try container.encode(rawValue)
    }
}

Usage:

enum FreeType: Int, CodableEnumeration {
        
    case normal = 1
    case free = 2
    
    static var defaultCase: Self { .normal }
}

let type: FreeType?

Support for Failables?

let json = """ [ {"name": "Taro", "age": 20}, {"name": "Hanako"} ] """.data(using: .utf8)! // Hanako has no "age"

Decoding Date?

I have a field updatedON that comes as a nil value from the api, but gets updated in the local DB after an object is updated. Not sure how to handle this. Essentially I'd like to combine ISO8601Strategy and DefaultCodableStrategy

@DefaultCodable<> not working as expected?

`
struct DefToZero: DefaultCodableStrategy {
static var defaultValue: Double { 0.0 }
}

struct T : Hashable, Equatable, Codable {
@DefaultCodable var a : Double = 0.0
@DefaultCodable var b : Double = 0.0
}

struct C : Hashable, Equatable, Codable {
var t : T
}
`

When I run this, however, I get...

CodingKeys(stringValue: "a", intValue: nil) in Context(codingPath: [CodingKeys(stringValue: "t", intValue: nil), debugDescription: "No value associated with key CodingKeys(stringValue: \"a\", intValue: nil) (\"a\").", underlyingError: nil)

I've simplified things a bit, but this retains the jist. Should nested structs like this work? Am I right in expecting this to decode from a JSON that is missing the a member of the substruct t?

SwiftCodable organization

Hi @marksands, thanks for such a great library! I'm currently thinking of making XMLCoder more community-driven to encourage more contributions. I'm considering an umbrella organization (could name it https://github.com/SwiftCodable, but suggestions are welcome) that could host multiple libraries related to Codable, including XMLCoder and possibly your library? I hope this would be more than the sum of all parts, since this would also help discoverability for potential users and cross-pollination across those libraries. Would you be interested in such collaboration?

LoselessValue property cannot be used in normal init

Currently when you have a class with a property marked as @LosslessValue you cannot initialize the class "directly" via a custom init method as the property marked as @LosslessValue can only by initialized as Decodable.

JSONEncoder's `dateEncodingStrategy` ignored for default `Date`

I'm using a JSONEncoder with dateEncodingStrategy set to .iso8601 like this:

extension JSONEncoder {
  public static var iso: JSONEncoder {
    let encoder = JSONEncoder()
    encoder.dateEncodingStrategy = .iso8601
    return encoder
  }
}

My Codable type has a discoverDate property which I specify as @DefaultCodable:

@DefaultCodable<Date.DefaultToNow>
public var discoverDate: Date

The DefaultCodableStrategy I use is defined like this (I'm using Date() as the default value):

extension Date {
  public enum DefaultToNow: DefaultCodableStrategy {
    public static var defaultValue: Date { Date() }
  }
}

But for some reason, when I encode the object using JSONEncoder.iso, it ignores the dateEncodingStrategy which I set to iso8601 but it renders like I didn't set it. For example, my JSON output looks something like this:

{ "discoverDate" : 7200 }

But I would expect it to look like this:

{ "discoverDate" : "2001-01-01T02:00:00Z" }

I believe this is a bug. Any ideas how I can work around this?

Create a new release

The last commit occurred on Sep 12, 2022

0.4.0 was released on Jul 10, 2021
ยท 8 commits to master since this release

Would you be open to making a new release? In a newer build system I'm using, I kind of need a proper tag and cannot just point at a SHA. While I could obviously fork the repo to (just) create a tag, I'd prefer the canonical repo.

Thanks!

Missing property in nested object fails the whole decoding of nested object

Hi, I have a following sample structure of the object:

struct A: Codable {
    .
    .
    .
    @LossyOptional var objectB: B?
    .
    .
    .
}

struct B: Codable {
    .
    .
    .
    @LossyOptional var details: String?
    .
    .
    .
}

I am trying to decode object of type A and in case there is a missing details key in object B, I always get the whole B as a nil.
Shouldn't decoding make details equal to nil and continue to decode the rest of the properties?
Or am I getting it wrong?

LosslessValue not working as excepted from Int to String value

Hi,

I've found an issue when using @LosslessValue from Int response.
We are currently upgrading our backend to a new API version and we need to handle both Int and String value for the id field.

To handle it properly we decided to set the id field with the @LosslessValue Property Wrapper.

Issue

struct Response: Codable {
    @LosslessValue var id: String
}

let json = #"{ "id": 1 }"#.data(using: .utf8)!
let result = try JSONDecoder().decode(Response.self, from: json)

print(result) // Response(id: "true")

The result for this example should be Response(id: "1") instead of Response(id: "true")
Note that everything works as excepted for int value > 1

Cause

This seems because of the decode priority in the types array in LosslessValue.swift

let types: [(Decoder) -> LosslessStringCodable?] = [
                decode(String.self),
                decodeBoolFromNSNumber(),
                decode(Bool.self),
                decode(Int.self),
                decode(Int8.self),
                decode(Int16.self),
                decode(Int64.self),
                decode(UInt.self),
                decode(UInt8.self),
                decode(UInt16.self),
                decode(UInt64.self),
                decode(Double.self),
                decode(Float.self)
            ]

As we can see the decodeBoolFromNSNumber() takes priority over the decode(Int.self)

[Question] Working with optional

Is there a way to combine the property wrapper with optionality?

Say I have

struct S {
     let timestamp: Date?
}

I want the timestamp property to be decoded using a custom strategy, but

struct S {
     @DateValue<TimestampStrategy> var timestamp: Date?
}

does not build because the timestamp property is optional.

Is there a way to make it work?

I tried defining a date strategy on optional types but that does not work.

Are you prepared to support field mapping?

BetterCodable is very cool library, but I noticed that field mapping doesn't seem to be supported now? I have to keep the model fields and json fields unified, which may not be a good fit for our project.

Do you have any plans to support field mapping? This would help us to completely ditch the custom Codable implementation.

Using DefaultCodable as an example, you can add an optional String type parameter to the init(wrappedValue:) method.

Afterwards, the init(from:) and encode(to:) methods decide whether to use the singleValueContainer or container(keyedBy:) method depending on whether the field has a value or not.

[Feature request] LosslessArray

Similar to LosslessValue, just on working or arrays.

Example:

struct S {
    @LoselessArray var groupIds: [String]
}

would decode

{
    "groupIds": ["1","2","3"]
}

and also

{
    "groupIds": [1,2,3]
}

SnakeCaseCodingKey And CamelCase handling?

let json = """ {"user_id": "abc", "name": "tattn"} """.data(using: .utf8)!

For Upper Camel Case:

let json = """
{"UserId": "abc", "Name": "tattn"}
""".data(using: .utf8)!

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.