thoughtbot/Argo

Error decoding array of optionals. Need all/something decoding operation

nmiyasato opened this issue · 6 comments

I have an array of elements that sometimes every now and then, one of them fail to parse. I would need the filtered array of successfully parse elements, but I get a failed operation one.

Sample JSON

{
  "elements": [
    {
      "property": "one"
    }, 
    {
      "property" : 1234
    }, 
    {
      "property" : "two"
    }
  ]
}

Models

All models involved, including their Decodable implementations.

import Argo
import Curry
import Runes

class MyPropertyClass {
    
    var property:String

    init(value:String) {
        self.property =  value
    }
}

extension MyPropertyClass: Decodable {
    internal static func decode(_ j: JSON) -> Decoded<MyPropertyClass> {
        return curry(MyPropertyClass.init)
            <^> j <| "property"
    }
}
import Argo
import Curry
import Runes

class MyProperties {
    
    var properties:[MyPropertyClass]

    init(values:[MyPropertyClass]) {
        self.properties =  values
    }
}

extension MyProperties: Decodable {
    internal static func decode(_ j: JSON) -> Decoded<MyProperties> {
        return curry(MyProperties.init)
            <^> j <|| "elements"
    }
}

That example above, Argo currently currently fails (because of the property:123) as it's an all or nothing operation.

I would need it to return an array with the first and last elements, which are the valid ones, discarding the invalid one.

We tried the solution described in the issue-449 but failed. Here is the error:

Cannot convert value of type '(MyProperties) -> (JSON) -> Decoded<[_?]>' to expected argument type '(_) -> Decoded<_>'

This is the class that we implemented

class MyProperties {
    
    var properties:[MyPropertyClass]
    
    init(values:[MyPropertyClass]) {
        self.properties =  values
    }
}

extension MyProperties: Decodable {
    internal static func decode(_ j: JSON) -> Decoded<MyProperties> {
        return curry(MyProperties.init)
            <^> (j <|| "elements" >>- decodeOptionalArray)
    }
    
    func values<T>(xs: [Decoded<T>]) -> [T?] {
        return xs.map { $0.value }
    }
    
    func decodeOptionalArray<T: Decodable>(j: JSON) -> Decoded<[T?]> where T.DecodedType == T {
        switch j {
        case let .array(a): return pure(values(xs: a.map(T.decode)))
        default: return .typeMismatch(expected: "Array", actual: j)
        }
    }
}

Please not that I do not need an array of optionals. I want a filtered one with all valid decodes json values.

Thanks

Hi, @nmiyasato
The issus of json is that the value is String or int, but your code don't implment it.
So I think firstly arrtribute property in MyPropertyClass should change like this:

class MyPropertyClass {
    
    var property: String?

    init(value: String?) {
        self.property =  value
    }
}

extension MyPropertyClass: Decodable {
    internal static func decode(_ j: JSON) -> Decoded<MyPropertyClass> {
        return curry(MyPropertyClass.init)
            <^> j <|? "property"
    }
}

And MyProperties may look like this:

class MyProperties {
    
    var properties:[MyPropertyClass]
    
    init(values:[MyPropertyClass]) {
        self.properties =  values
    }
}

extension MyProperties: Decodable {
    static func decode(_ j: JSON) -> Decoded<MyProperties> {
        guard let array: [MyPropertyClass] = (j <|| "elements").value?.filter({ $0.property != nil }) else {
            return .typeMismatch(expected: "Array", actual: j)
        }

        return pure(MyProperties(values: array))
    }
}

Does this solve your problem?

Actually the value of property must be string... but somehow the server returns an invalid json, so I want to parse all the valid ones. I know that your solution might work, but I don't want to have an optional String in the MyPropertyClass

If MyPropertyClass has more attributes, when serializing that json, it should be success, or ignore it? If there must be String, then I have another idea.

In MyPropertyClass:

class MyPropertyClass {
    
    var property: String

    init(value: String) {
        self.property =  value
    }
}

extension MyPropertyClass: Decodable {
    internal static func decode(_ j: JSON) -> Decoded<MyPropertyClass> {
        let valueWithDefault: String = (j <| "property").value ?? ""
        let myClass = MyPropertyClass(value: valueWithDefault)
        return pure(myClass)
    }
}

In MyProperties:

class MyProperties {
    
    var properties:[MyPropertyClass]
    
    init(values:[MyPropertyClass]) {
        self.properties =  values
    }
}

extension MyProperties: Decodable {
    static func decode(_ j: JSON) -> Decoded<MyProperties> {
        guard let array: [MyPropertyClass] = (j <|| "elements").value?.filter({ !$0.property.isEmpty }) else {
            return .typeMismatch(expected: "Array", actual: j)
        }

        return pure(MyProperties(values: array))
    }
}

Hey, thanks for replying... :)

The MyPropertyClass is just an example... it can have all sorts of properties, objects, etc... The main idea is that, given the initial json, it should ignore the ones that are invalid and serialize the valid ones.

I could do some sort of hack, by using some sort of fallback empty class and then filter those... but I was looking for something more succint.

So we finally came with a solution to our problem. The main idea is to somehow filter those invalid objects... here is the code

extension Collection where Iterator.Element: Decodable, Iterator.Element == Iterator.Element.DecodedType {
    
    static func fullDecode(_ json: JSON) -> Decoded<[Iterator.Element]> {
        switch json {
        case let .array(a): return filteredSequence(a.map(Iterator.Element.decode))
        default: return .typeMismatch(expected: "Array", actual: json)
        }
    }
}

infix operator <||* : ArgoDecodePrecedence

public func <||* <A: Decodable>(json: JSON, key: String) -> Decoded<[A]> where A == A.DecodedType {
    return json <||* [key]
}

public func <||* <A: Decodable>(json: JSON, keys: [String]) -> Decoded<[A]> where A == A.DecodedType {
    return flatReduce(keys, initial: json, combine: decodedJSON) >>- Array<A>.fullDecode
}

public func filteredSequence<T>(_ xs: [Decoded<T>]) -> Decoded<[T]> {
    var accum: [T] = []
    accum.reserveCapacity(xs.count)
    
    for x in xs {
        switch x {
        case let .success(value): accum.append(value)
        default: break; // <------------- We don't fail, we just continue.
        }
    }
    
    return pure(accum)
}

I think might be useful for the Argo library... do you want me to do a patch and send you a pull request?

@nmiyasato sorry for the delay in response here. I think the concept you're describing is a function called catMaybes in Haskell which one could directly translate to catOptionals in Swift where:

func catOptionals<T>(_ optionals: [T?]) -> [T]

Since we're working with Decoded in Argo instead of Optional, you could make a function catDecoded which we have done.

You can see that this function takes an array of decoded values and returns you an array of only the successfully decoded ones. One final thing you need to do is call pure to wrap that array back into a Decoded type. I think you should be able to use this function in place of filteredSequence as long as you bring back the pure from decodeOptionalArray in your first example.