skip to Main Content

I am trying to convert the Landmarks app to SwiftData… why isn’t this class conforming to codable/decodable? It won’t compile but the messages are non-specific:

"Type ‘Landmark’ does not conform to protocol ‘Decodable’"
"Type ‘Landmark’ does not conform to protocol ‘Encodable’"

and

In expansion of macro ‘Model’ here:
"Cannot automatically synthesize ‘Decodable’ because ‘any SwiftData.BackingData’ does not conform to ‘Decodable’"

import Foundation
import SwiftUI
import CoreLocation
import SwiftData

@Model
final class Landmark: Hashable, Codable, Identifiable {
    var id: Int
    var name: String
    var park: String
    var state: String
    var dezcription: String
    var isFavorite: Bool
    var isFeatured: Bool
    
    var category: Category
    private var coordinates: Coordinates
    private var imageName: String
    
    init(id:Int = Int.random(in: 2000...Int.max), name:String = "", park: String = "", state:String = "", dezcription: String = "", isFavorite: Bool = false, isFeatured:Bool = false, category: Category = Category.mountains, coordinates: Coordinates = Coordinates(latitude: 0, longitude: 0), imageName:String = "umbagog") {
        self.id = id
        self.name = name
        self.park = park
        self.state = state
        self.dezcription = dezcription
        self.isFavorite = isFavorite
        self.isFeatured = isFeatured
        self.category = category
        self.coordinates = coordinates
        self.imageName = imageName
        
    }

    
    enum Category: String, CaseIterable, Codable {
        case lakes = "Lakes"
        case rivers = "Rivers"
        case mountains = "Mountains"
    }

    var image: Image {
        Image(imageName)
    }
    
    
    var locationCoordinate: CLLocationCoordinate2D {
        CLLocationCoordinate2D(
            latitude: coordinates.latitude,
            longitude: coordinates.longitude)
    }

    struct Coordinates: Hashable, Codable {
        var latitude: Double
        var longitude: Double
    }
}

PS – I know that the design of this (the random int id… the default location of 0,0) are bad… but that’s not the point of the question.

I’ve tried:

  • Commenting out/isolating potentially problematic types
  • Making sure everything is codable
  • Including all vars in the initializer
  • Reading the dos and donts of declaring a @Model class
  • Making the Image var @Transient (although it is computed anyway so that should not be necessary)
  • Moving the Coordinates struct and Category enum higher in scope (out of this class)
  • Adding a "real" UUID variable that actually is guaranteed unique

3

Answers


  1. Chosen as BEST ANSWER

    Sorry, this is a case where I made a false assumption in my question — That this class needs to be codable.

    The reason the class was marked Codable was because that was the way it was in the original Landmarks app and I didnt think to change it.

    What I didn't realize is that a class marked with @Model does not require any explicit protocols, and in fact should not be marked codable unless one plans on doing some serious surgery as in Sweeper's answer.

    If I simply remove the Codable protocol (and can also remove all other protocols safely), it works.

    I enterpreted the error message as a Swiftdata error when it is just a “normal” error saying “this class is not codable [because of Swiftdata macro]”

    So the other previous answers are appreciated and valid if anyone out there actually has a class they need to be a swift data model and codable, but in my case it was just a false assumption.

    One could say that this renders the question invalid, but that is hindsight and I think this the answer to my question, and the information will be valuable to people with the same misunderstanding.


  2. The reason this happens is that the synthesis of the Codable implementation happens after the macros have expanded.

    After the macros have expanded, all the properties you declared become computed properties, and the macros add an additional _$backingData property, as well as an underscored-prefixed version of each property you declared, of type _SwiftDataNoType (just an empty struct, acting as a placeholder), among other things.

    Basically, @Model turns this:

    class Foo {
        var foo: Int
        var bar: String
    }
    

    into:

    class Foo: PersisrentModel {
        var foo: Int { get { ... } set { ... } }
        var bar: String { get { ... } set { ... } }
        private var _foo: _SwiftDataNoType
        private var _bar: _SwiftDataNoType
    
        private var _$backingData: any SwiftData.BackingData<Foo>
    }
    

    No wonder the Codable synthesis fails!

    I recommend checking out the actual code that it generates (the above is only a simplified version) with Xcode’s "Inline Macro" and "Expand Macro" context menu items.

    You can conform to Codable manually, like this article shows. See also workingdog’s answer.

    Alternatively, try writing a macro that generates a Codable implementation. Your macro would receive the same syntax tree as @Model, so you will have access to the class’ members before the macro expansion. You’d use it like this:

    @Model
    @CodableModel
    final class Landmark: Hashable, Identifiable {
        var id: Int
        var name: String
        ...
    }
    

    Here is a rough sketch of such a CodableModel:

    @attached(member, names: named(init(from:)), named(encode(to:)), named(CodingKeys))
    @attached(extension, conformances: Codable)
    public macro CodableModel() = #externalMacro(module: "...", type: "CodableModel")
    
    // ...
    
    public struct CodableModel: MemberMacro, ExtensionMacro {
        public static func expansion(
          of node: AttributeSyntax,
          providingMembersOf declaration: some DeclGroupSyntax,
          in context: some MacroExpansionContext
        ) throws -> [DeclSyntax] {
            var properties = [(type: TypeSyntax, name: TokenSyntax)]()
            let members = declaration.memberBlock.members
            for member in members {
                guard let propertyDecl = member.decl.as(VariableDeclSyntax.self) else { continue }
                guard propertyDecl.bindingSpecifier.text == "var" else { continue } // ignore lets
                guard let binding = propertyDecl.bindings.first else { continue }
                guard binding.accessorBlock == nil else { continue } // ignore computed properties
                guard let type = binding.typeAnnotation?.type else { continue }
                guard let name = binding.pattern.as(IdentifierPatternSyntax.self)?.identifier else { continue }
                properties.append((type, name))
            }
            let codingKeys = try EnumDeclSyntax("enum CodingKeys: String, CodingKey") {
                for (_, name) in properties {
                    "case (name)"
                }
            }
            
            let initialiser = try InitializerDeclSyntax("init(from decoder: Decoder) throws") {
                "let container = try decoder.container(keyedBy: CodingKeys.self)"
                for (type, name) in properties {
                    "(name) = try container.decode((type).self, forKey: .(name))"
                    // TODO: use decodeIfPresent if the type is optional
                }
            }
            
            let encodeMethod = try FunctionDeclSyntax("func encode(to encoder: Encoder) throws") {
                "var container = encoder.container(keyedBy: CodingKeys.self)"
                for (_, name) in properties {
                    "try container.encode((name), forKey: .(name))"
                }
            }
            
            return [
                DeclSyntax(codingKeys),
                DeclSyntax(initialiser),
                DeclSyntax(encodeMethod),
            ]
        }
        
        public static func expansion(
            of node: AttributeSyntax,
            attachedTo declaration: some DeclGroupSyntax,
            providingExtensionsOf type: some TypeSyntaxProtocol,
            conformingTo protocols: [TypeSyntax],
            in context: some MacroExpansionContext
        ) throws -> [ExtensionDeclSyntax] {
            [try ExtensionDeclSyntax("extension (type): Codable") {}]
         }
    }
    
    Login or Signup to reply.
  3. Try this approach of using a specific public init(from decoder: Decoder) and
    public func encode(to encoder: Encoder) to make your Landmark Codable.

    Note you cannot have var image: Image, Image is a View and should not be part of your model.
    Similarly, var locationCoordinate: CLLocationCoordinate2D {...} is not Codable, use a function
    instead, as shown in the example code.

    @Model
    final class Landmark: Hashable, Codable, Identifiable {
        var id: Int
        var name: String
        var park: String
        var state: String
        var dezcription: String
        var isFavorite: Bool
        var isFeatured: Bool
        
        var category: Category
        private var coordinates: Coordinates
        private var imageName: String
        
        enum CodingKeys: CodingKey {
            case id, name, park, state, dezcription, isFavorite, isFeatured
            case category, coordinates, imageName
        }
        
        init(id:Int = Int.random(in: 2000...Int.max), name:String = "", park: String = "", state:String = "", dezcription: String = "", isFavorite: Bool = false, isFeatured:Bool = false, category: Category = Category.mountains, coordinates: Coordinates = Coordinates(latitude: 0, longitude: 0), imageName:String = "umbagog") {
            
            self.id = id
            self.name = name
            self.park = park
            self.state = state
            self.dezcription = dezcription
            self.isFavorite = isFavorite
            self.isFeatured = isFeatured
            self.category = category
            self.coordinates = coordinates
            self.imageName = imageName
        }
        
        enum Category: String, CaseIterable, Codable {
            case lakes = "Lakes"
            case rivers = "Rivers"
            case mountains = "Mountains"
        }
        
        //    var image: Image {
        //        Image(imageName) // <--- should NOT be here, Image is a View
        //    }
        
        // not Codable, use a function
        //    var locationCoordinate: CLLocationCoordinate2D {
        //        CLLocationCoordinate2D(
        //            latitude: coordinates.latitude,
        //            longitude: coordinates.longitude)
        //    }
        
        func locationCoordinate() -> CLLocationCoordinate2D {
            CLLocationCoordinate2D(
                latitude: coordinates.latitude,
                longitude: coordinates.longitude)
        }
        
        struct Coordinates: Hashable, Codable {
            var latitude: Double
            var longitude: Double
        }
    
        public init(from decoder: Decoder) throws {
            let container = try decoder.container(keyedBy: CodingKeys.self)
            id = try container.decode(Int.self, forKey: .id)
            name = try container.decode(String.self, forKey: .name)
            park = try container.decode(String.self, forKey: .park)
            state = try container.decode(String.self, forKey: .state)
            dezcription = try container.decode(String.self, forKey: .dezcription)
            isFavorite = try container.decode(Bool.self, forKey: .isFavorite)
            isFeatured = try container.decode(Bool.self, forKey: .isFeatured)
            category = try container.decode(Category.self, forKey: .category)
            coordinates = try container.decode(Coordinates.self, forKey: .coordinates)
            imageName = try container.decode(String.self, forKey: .imageName)
        }
        
        public func encode(to encoder: Encoder) throws {
            var container = encoder.container(keyedBy: CodingKeys.self)
            try container.encode(id, forKey: .id)
            try container.encode(name, forKey: .name)
            try container.encode(park, forKey: .park)
            try container.encode(state, forKey: .state)
            try container.encode(dezcription, forKey: .dezcription)
            try container.encode(isFavorite, forKey: .isFavorite)
            try container.encode(isFeatured, forKey: .isFeatured)
            try container.encode(category, forKey: .category)
            try container.encode(coordinates, forKey: .coordinates)
            try container.encode(imageName, forKey: .imageName)
        }
        
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search