Swift 4 Codable decode URL from String

Viewed 8782

I am currently trying to decode Property list using PropertyListEncoder and Swift 4 Codable protocol.

I tried some basic types (Strings, Ints, ..) and these all works just fine, but I am not able decode URL. I read multiple articles on this topic and I am pretty sure this should just work. However, the following example fails the decoding with this error:

Expected to decode Dictionary<String, Any> but found a string/data instead.

I think this works correctly with .json files and I didn't find any informations about different support for codable types in JSONDecoder and PropertyListDecoder. Could this be caused by parser incompatibility?

I am using Xcode 9.1 and Swift 4.0.2.

Sample .plist:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
    <dict>
        <key>web</key>
        <string>https://link.to</string>
    </dict>
</plist>

Sample Swift code:

struct Info: Codable {
   let web: URL
}

func loadInfo() {
    let propertiesDecoder = PropertyListDecoder()
    let data = try! Data(contentsOf:
        Bundle.main.url(forResource: "web", withExtension: "plist")!)

    try! propertiesDecoder.decode(Info.self, from: data)
}

Thanks for any help!

3 Answers

As I can see the key web has a value of type String so we need to keep a matching type

But to get the string as a URL then we need to add a computed variable and maybe to optimize it a little bit we can make the variable lazy i.e it will be calculated once when it's needed

The edited strut would look like this:

struct Info: Codable {
    let web: String

    lazy var url: URL? = { return URL(string: web) }()
}

Assuming the URL in your JSON is properly formatted, you should be able to manually decode from String to URL like this with Swift 5:

let json = """
    {
        "name": "Dissent",
        "url": "https://www.dissentmagazine.org"
    }
"""

func hexAConversion(from hex: String)->Int {
    return Int(Int32(bitPattern: (UInt32(hex, radix: 16)!)))
}

let data = json.data(using: String.Encoding.utf8)!

let decoder = JSONDecoder()

struct CodeMe: Decodable {
  var name: String
  var url: URL?
    
  enum CodingKeys: String, CodingKey {
    case name, url
  }
    
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.name = try container.decode(String.self, forKey: .name)
        self.url = URL(string: try container.decode(String.self, forKey: .url))
    }
  }

let codeMe = try! decoder.decode(CodeMe.self, from: data)

print(codeMe.url) // prints: Optional(https://www.dissentmagazine.org)

Using a similar technique you could also have a custom encoding strategy to make your object conform to Codable instead of Decodable. See https://stackoverflow.com/a/65315622/713077 for more details.

Related