Having trouble with RawRespresentable "Expected to decode String but found a dictionary instead."

I want to use AppStorage for a custom struct I am using

struct Activities {
    var name: String
    var age: Int
}
struct ContentView: View {
    @AppStorage("key") private var activities: Activities = .init(name: "Albert", age: 42)

    var body: some View {
        VStack {
            TextField("Activity Name", text: $activities.name)
        }
    }
}

The above code generates a compiler warning, recommending I add RawRepresentable conformance. So I've added it like this:

extension Activities: RawRepresentable {
    public init?(rawValue: String) {
        guard let data = rawValue.data(using: .utf8) else {
            return nil
        }
        do {
            let result = try JSONDecoder().decode(Activities.self, from: data)
            self = result
        }
        catch {
            print(error)
            return nil
        }
    }
    var rawValue: String {
        guard let data = try? JSONEncoder().encode(self),
              let result = String(data: data, encoding: .utf8) else {
            return "{}"
        }
        return result
    }
}

This leads to a stack overflow because calling encode from rawValue calls rawValue. :-( I overcame this by declaring Codable conformance and overriding the default Encodable implementation:

extension Activities: Codable {
    enum CodingKeys: String, CodingKey {
        case name
        case age
    }
    func encode(to encoder: any Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(name, forKey: .name)
        try container.encode(age, forKey: .age)
    }
}

This solves the stack overflow, but now init?(rawValue: String) is failing and I'm not sure why. When I set a breakpoint in my catch block I see the following:

(lldb) po error
▿ DecodingError
  ▿ typeMismatch : 2 elements
    - .0 : Swift.String
    ▿ .1 : Context
      - codingPath : 0 elements
      - debugDescription : "Expected to decode String but found a dictionary instead."
      - underlyingError : nil

(lldb) po rawValue
{"name":"Albert2","age":42}
(lldb) po data
▿ 27 bytes
  - count : 27
  ▿ bytes : 27 elements
    - 0 : 123
    - 1 : 34
    - 2 : 110
    - 3 : 97
    - 4 : 109
    - 5 : 101
    - 6 : 34
    - 7 : 58
    - 8 : 34
(truncated to save space for posting :-)
Answered by DTS Engineer in 886205022

I’m not 100% sure what’s causing all the behaviour you’re seeing [1] but if I were in your shoes I’d get around this by declaring an codable version of your struct solely for the benefit of the coding system:

struct Activities {
    var name: String
    var age: Int
    
    fileprivate struct Inner: Codable {
        var name: String
        var age: Int
    }
}

extension Activities: RawRepresentable {

    init?(rawValue: String) {
        guard let i = try? JSONDecoder().decode(Inner.self, from: Data(rawValue.utf8)) else {
            return nil
        }
        self.init(name: i.name, age: i.age)
    }
    
    var rawValue: String {
        let i = Inner(name: self.name, age: self.age)
        let e = try! JSONEncoder().encode(i)
        return String(decoding: e, as: UTF8.self)
    }
}

The downside is that you need to keep Activities and Activities.Inner in sync, but the benefit is that the extra code is super simple.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

[1] I think it’s because the encoding system has special cases for types with a raw value, but I haven’t actually dug into that properly.

I’m not 100% sure what’s causing all the behaviour you’re seeing [1] but if I were in your shoes I’d get around this by declaring an codable version of your struct solely for the benefit of the coding system:

struct Activities {
    var name: String
    var age: Int
    
    fileprivate struct Inner: Codable {
        var name: String
        var age: Int
    }
}

extension Activities: RawRepresentable {

    init?(rawValue: String) {
        guard let i = try? JSONDecoder().decode(Inner.self, from: Data(rawValue.utf8)) else {
            return nil
        }
        self.init(name: i.name, age: i.age)
    }
    
    var rawValue: String {
        let i = Inner(name: self.name, age: self.age)
        let e = try! JSONEncoder().encode(i)
        return String(decoding: e, as: UTF8.self)
    }
}

The downside is that you need to keep Activities and Activities.Inner in sync, but the benefit is that the extra code is super simple.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

[1] I think it’s because the encoding system has special cases for types with a raw value, but I haven’t actually dug into that properly.

Having trouble with RawRespresentable "Expected to decode String but found a dictionary instead."
 
 
Q