josefdolezal
josefdolezal

Reputation: 493

Swift 4 Codable decode URL from String

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!

Upvotes: 6

Views: 12299

Answers (3)

Interior Night
Interior Night

Reputation: 1225

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.

Upvotes: 2

Eric Yanush
Eric Yanush

Reputation: 404

You can store URLs in Property Lists, and use the default Decodable implementation.

Based on the implementation of the Decodable protocol init in the Swift standard library, you must store the URL as a dictionary in the Plist, in a key called "relative". Optionally, you can also include a nested URL dictionary named "base" in the dictionary. These two values will get passed to the URL(string: relative, relativeTo: base) constructor during decoding.

For example:

<?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">
<array>
    <dict>
        <key>apiHost</key>
        <dict>
            <key>relative</key>
            <string>https://example.com/api/v0</string>
        </dict>
        <key>otherKey</key>
        <string>SomeValue</string>
       </dict>
 </array>
 </plist>

This Plist will decode using the following:

struct PlistItem: Codable {
    let apiHost: URL
    let otherKey: String
}
guard let filePath = Bundle.main.url(forResource: "Data", withExtension: "plist") else { return }
do {
    let data = try Data(contentsOf: filePath)
    let decoder = PropertyListDecoder()
    let values = try decoder.decode([PlistItem].self, data)
}
catch { }

Upvotes: 22

zombie
zombie

Reputation: 5259

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) }()
}

Upvotes: 2

Related Questions