skip to Main Content

I’m trying to implement a custom init(from decoder: Decoder) throws {} for the Decodable protocol, but I get an error:

DecodingError.typeMismatch(Swift.Dictionary<Swift.String, Foundation.JSONValue>)
Expected to decode Dictionary<String, JSONValue> but found a string instead.
struct Model: Decodable {
  let title: String
  let gotoAction: GotoAction // String work fine
}

enum GotoAction: Decodable {
  case noAction
  case websiteLink(String)
  case sheet(OpenSheet)

  private enum CodingKeys: String, CodingKey {
    case noAction
    case websiteLink
    case sheet
  }

   init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)

       if let link = try container.decodeIfPresent(String.self, forKey: .websiteLink) {
        self = .websiteLink(link)
       }

       if let sheet = try container.decodeIfPresent(OpenSheet.self, forKey: .sheet) {
        self = .sheet(sheet)
       }

      //  let noAction = try container.decodeIfPresent(String.self, forKey: .noAction)
      //  self = noAction

      throw DecodingError.dataCorruptedError(forKey: .websiteLink, in: container, debugDescription: "No match")
    }
}

enum OpenSheet: Decodable {
  case firstBanner
  case secondBanner(String)
}

let json = """
{
  "title": "Hello",
  "goto_action": "no_action"
}
"""

let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase

let result = try decoder.decode(Model.self, from: json.data(using: .utf8)!)
print(result)

What do I need to do to make the code work correctly ?

2

Answers


  1. I don´t think this can be done with JsonDecoder alone. Essentially the value of gotoAction is of type String and not a valid Json.

    That´s the meaning of the error message you get.

    You would need to interpret and convert this string to the enums yourself.

    I´ve implemented a possible solution with the information you provided for GotoAction enum. Of course this needs more work like checking for the correct keys and throwing errors appropriately.

    struct Model: Decodable {
        let title: String
        let gotoAction: GotoAction // String work fine
        
        private enum CodingKeys: String, CodingKey{
            case title
            case gotoAction
        }
        
        init(from decoder: Decoder) throws {
            let container = try decoder.container(keyedBy: CodingKeys.self)
            self.title = try container.decode(String.self, forKey: .title)
            
            let enumString = try container.decode(String.self, forKey: .gotoAction)
            self.gotoAction = try GotoAction.fromString(enumString)
        }
        
    }
    
    enum GotoAction: Decodable {
        case noAction
        case websiteLink(String)
        case sheet(OpenSheet)
        
        static func fromString(_ enumString: String) throws -> GotoAction{
            
            let array = enumString.split(separator: "/")
            let key = array.first!
            let associatedValue = array.dropFirst().joined(separator: "/")
    
            if key == "website_link"{
                return .websiteLink(associatedValue)
            }
            else if key == "sheet"{
                return .sheet(try OpenSheet.fromString(associatedValue))
            }
            
            return .noAction
        }
    }
    
    Login or Signup to reply.
  2. You need to extract parts from the string corresponding to the goto_action key. This is no longer parsing JSON. You need to write your own logic for parsing things like website_link/foobarbaz and open_sheet/second_banner/foo, and turn that into a value of GotoAction.

    The init(from:) for GotoAction can be written like this:

    init(from decoder: Decoder) throws {
        func decodeOpenSheet(_ str: String) throws -> OpenSheet {
            if str == "first_banner" {
                return .firstBanner
            } else if let range = str.range(of: "second_banner/") { // assuming this is how the second banner would be written...
                return .secondBanner(String(str[range.upperBound...]))
            }
            throw DecodingError.typeMismatch(
                OpenSheet.self,
                    .init(codingPath: decoder.codingPath, debugDescription: "Unknown sheet")
            )
        }
        
        let str = try String(from: decoder)
        if str == "no_action" {
            self = .noAction
        } else if let range = str.range(of: "website_link/") {
            self = .websiteLink(String(str[range.upperBound...]))
        } else if let range = str.range(of: "sheet/") {
            self = .sheet(try decodeOpenSheet(String(str[range.upperBound...])))
        } else {
            throw DecodingError.typeMismatch(
                GotoAction.self,
                    .init(codingPath: decoder.codingPath, debugDescription: "Unknown GotoAction")
            )
        }
    }
    

    Note that OpenSheet doesn’t need to be Decodable anymore. It only occurs as a substring of a larger JSON string, which you extract by manually manipulating the JSON string.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search