Error checking Decodable for plists

Using the Decodable protocol to read plist data my app now crashes if there is a mismatch with the enum statements, before the data would just be ignored:

func parsePropertyListForBeer(name: String) {
    
    let path = Bundle.main.path(forResource: "BeerData", ofType: "plist")
    let dict: NSDictionary = NSDictionary(contentsOfFile: path!)!
    
    if dict.object(forKey: "Stout") != nil {
        if let stoutDict: [String : Any] = dict.object(forKey: "Stout") as? [String : Any] {
            for (key, value) in stoutDict {
                if key == name {
                    stoutBeerInfo(beerData: value as! [String : Any])
                }
            }
        }
    }
}
enum Characterists : String, Decodable {
    case blonde
    case brown
}

struct SortBeer: Decodable {
    private enum CodingKeys: String, CodingKey {
        case name, type, age
    }
    
    let name: String
    let type: Characterists.RawValue
    let age: Int
}

func distillBeerData() {
    let plistData = Bundle.main.url(forResource: "BeerData", withExtension: "plist")!
    let data = try! Data(contentsOf: plistData)
    let decoder = PropertyListDecoder()
    return try! decoder.decode(SortBeer.self, from: data)
}

I would like to use all of the safety options in Swift to avoid forcing anything, but reading up on Swift 5 it seems that I must preconceive every problem with Result types. Focusing on Do, Try, Catch to begin with I found that error checking was going to be pretty laborious:

enum PlistError: Error {
    case plistMissing
}

extension PlistError: LocalizedError {
    public var errorDescription: String? {
        switch self {
        case .plistMissing:
            return "Plist is missing!"
        }
    }
}
func distillBeerData() {
    do {
        let plistData = Bundle.main.url(forResource: "BeerData", withExtension: "plist")
    }
    catch let error as PlistError {
        print("Error reading plist: \(error)")
        print(error.localizedDescription)
    }
    
    let data = try! Data(contentsOf: plistData)
    let decoder = PropertyListDecoder()
    return try! decoder.decode(SortBeer.self, from: data)
}

I am unsure how to proceed further, even before looking into Return types.

Do you have some data to test with, as well as what is expected vs what actually happened from said data?

It's just dummy values that are in the enum, just something simple to start with. However, my real data set contains nested Dictionaries etc..

I'm more focused on error handling.

You can just don't catch the error, and let them propagate up, might as well throw new ones while you're at it. You can then catch one at the very end, or where you know how to handle them better than just print and bail out.

func distillBeerData() throws -> SortBeer {
    let plistData = Bundle.main.url(forResource: "BeerData", withExtension: "plist")
    let data = try Data(contentsIf: plistData)
    return try decoder.decode(SortBeer.self, from: data)
}

I suspect this to be because the data structure in plist file doesn't match what you assume (in SortBeer structure).

Btw, I checked your parsePropertyListForBeer and distillBeerData. The plist files structure are different, meaning distillBeerData will never get any data, not the one parsePropertyListForBeer is using.

I don't know your actual plist file, so I can't help fixing that, though hope this helps.

Thank you for your reply.

I get errors with the code without force unwrapping:

    func distillBeerData() throws -> Config {
        let plistData = Bundle.main.url(forResource: "BeerData", withExtension: "plist")
        let data = try Data(contentsOf: plistData)   //error: Value of optional type 'URL?' must be unwrapped to a value of type 'URL'
        return try decoder.decode(Config.self, from: data)   //error: Use of unresolved identifier 'decoder'
    }

The examples are different in the data they manage, but you can see the need to upgrade the structure to something safer and more Swift like. I can manage the data once I have it, but I just error handle.

Imagine the data was something simple like:

<?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>name</key>
	<string>Bernard</string>
	<key>color</key>
	<string>brown</string>
	<key>age</key>
	<integer>101</integer>
</dict>
</plist>

Ok, this should be a working example. Few things to note:

  • You're not handling the error, you're throwing it.
    You handle errors only when you know what to do (recover, silently ignore, crash the program, retry, etc.), otherwise, let the callee decide.
  • You don't need to do HairColor.RawValue, just HairColor. Swift is that smart.
  • Anything you want the decoder to silently ignore (if it's not in the file), make them Optional, like my SortBeer.nonExistentVariable
import Foundation

enum ParsingError: Error {
    case fileNotFound
}

enum HairColor: String, Decodable {
    case blonde, brown
}

struct SortBeer: Decodable {
    var name: String, hair: HairColor, age: Int, nonExistentVariable: String?
}

func distillBeerData() throws -> SortBeer {
    guard let plistURL = Bundle.main.url(forResource: "BeerData", withExtension: "plist") else {
        throw ParsingError.fileNotFound
    }

    let decoder = PropertyListDecoder()
    let data = try Data(contentsOf: plistURL)
    return try decoder.decode(SortBeer.self, from: data)
}

Many thanks, I see why decoder wasn't working now.

Terms of Service

Privacy Policy

Cookie Policy