2

This might be something really easy but I don't understand how to do it: so I have this DTO struct I use to get API data into it and map it to Model struct

my DTO:

struct PetDTO: Codable {
    var id: Int
    var category: CategoryDTO?
    var name: String?
    var photoUrls: [String]?
    var tags: [TagDTO]?
    var status: StatusDTO?
}

public class CategoryDTO: NSObject, Codable {
    var id: Int
    var name: String?
    
    private enum CodingKeys: String, CodingKey {
        case id = "id"
        case name = "name"
    }
    
    required 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)
    }
}

public class TagDTO: NSObject, Codable {
    var id: Int
    var name: String?
    
    private enum CodingKeys: String, CodingKey {
        case id = "id"
        case name = "name"
    }
    
    required 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)
    }
}

enum StatusDTO: String, Codable {
    case available
    case sold
    case pending
}

And my model:

struct PetDataModel {
    var id: Int
    var category: Category
    var name: String?
    var photoUrls: [String]?
    var tags: [Tags]?
    var status: Status?
    
    init(petDto: PetDTO) {
        self.id = petDto.id
        self.category = Category(categoryDto: petDto.category)
        self.name = petDto.name
        self.photoUrls = petDto.photoUrls
        for tag in petDto.tags ?? [] {
            self.tags = [Tags(tagDTO: tag)] // petDto?.map { Tags(tagDTO: $0) }
        }
        self.status = Status(rawValue: petDto.status?.rawValue)
    }
}

struct Category {
    var id: Int
    var name: String?
    
    init(categoryDto: CategoryDTO) {
        self.id = categoryDto.id
        self.name = categoryDto.name
    }
}

struct Tags {
    var id: Int
    var name: String?
    
    init(tagDTO: TagDTO) {
        self.id = tagDTO.id
        self.name = tagDTO.name
    }
}

enum Status: String, Codable {
    case available
    case sold
    case pending
}

As you can see, the mapping happens in Init of PetDataModel. I have errors on this lines

Please tell me how to fix this without making CategoryDto from PetDTO non optional, I need it to stay optional.

burnsi
  • 6,194
  • 13
  • 17
  • 27
Shum
  • 23
  • 5
  • Best is to define what a valid `PetDataModel` is like, i.e. which properties can be optional and which one are definitely mandatory. When you initialise the model, _throw an error_ when the model can not be initialised from the DTO because it omits mandatory properties and violates _invariance_. You throw an error, because this IS an error! Then immediately blame the backend developer! ;) And well, YES, it's unfortunate the frontend devs have to deal with wonky APIs which have not been tested and data in databases that are strictly corrupt ;) – CouchDeveloper Aug 21 '22 at 16:19
  • @CouchDeveloper, thanks for the advice. How do I know which properties are mandatory and what are not. I know for sure every Pet should have an id, and same with the other fields, they also seem to be mandatory for me... – Shum Aug 21 '22 at 16:46
  • This is always a question which you have to clarify. Here, "domain experts", the Product Owner, the Product Manager or your colleagues can help. If none is available, decide yourself. Ask yourself, if an object without a certain property makes sense, or rather not. Also note, that these optionals very likely never _meant_ to be optional, but are the result of some lacking best practices in the backend side using languages like JS, Java etc. – CouchDeveloper Aug 21 '22 at 16:57
  • One example which can be optional, is `photoUrls`. However, it's an array - so when there are zero urls why not send an empty array? A nil may mean an error somewhere not on your part. Better to fail early! :) Clear defined APIs are a pleasure to work with, wonky ones not exactly and cause a lot of costs. ;) – CouchDeveloper Aug 21 '22 at 17:02
  • @CouchDeveloper, it's strange that after I removed optional in my DTO for almost everything except photoUrls my request stopped working LOL – Shum Aug 21 '22 at 17:48
  • The DTO represents the API contract. It is as it is: keep the optionals if these are defined to be optional in the API. But your _Model_, which will be initialised from the DTO represents the _domain object_. The domain object may have other requirements. Ideally, the definition of the DTO should be able to initialise a valid Model. If not, the DTO should be fixed - which requires work on the backend, too. – CouchDeveloper Aug 22 '22 at 06:01

1 Answers1

0

You can make category form your PetDataModel optional too.

struct PetDataModel {
    var id: Int
    var category: Category?
    var name: String?
    var photoUrls: [String]?
    var tags: [Tags]?
    var status: Status?
    
    init(petDto: PetDTO) {
        self.id = petDto.id
        self.category = Category(categoryDto: petDto.category)
        self.name = petDto.name
        self.photoUrls = petDto.photoUrls
        for tag in petDto.tags ?? [] {
            self.tags = [Tags(tagDTO: tag)] // petDto?.map { Tags(tagDTO: $0) }
        }
        self.status = Status(rawValue: petDto.status?.rawValue)
    }
}

and make your initializer optional:

struct Category {
    var id: Int
    var name: String?
    
    init?(categoryDto: CategoryDTO?) {
        guard let categoryDto = categoryDto else{
            return nil
        }

        self.id = categoryDto.id
        self.name = categoryDto.name
    }
}

if you don´t want an optional initializer you can check and assign like this:

self.category = petDto.category != nil ? Category(categoryDto: petDto.category!) : nil
burnsi
  • 6,194
  • 13
  • 17
  • 27
  • Do you know what causes such errors: keyNotFound(CodingKeys(stringValue: "name", intValue: nil), Swift.DecodingError.Context(codingPath: [_JSONKey(stringValue: "Index 346", intValue: 346), CodingKeys(stringValue: "tags", intValue: nil), _JSONKey(stringValue: "Index 0", intValue: 0)], debugDescription: "No value associated with key CodingKeys(stringValue: \"name\", intValue: nil) (\"name\").", underlyingError: nil)) I've had the same with Category and I have it with Tags now, but I don't understand why they appear – Shum Aug 20 '22 at 20:17
  • my properties are optionals so they should be at least nil, they would just not appear on UI if I understand correctly. Why does it throw this error? – Shum Aug 20 '22 at 20:19
  • @Shum I know you are new to this site, so please try to understand how SO works. It is build like a knowledge collection so you have a particular question and an answer to that. This is clearly something that has to do with your decoding. And therefor not in the scope of this question. It would have fitted in the one you deleted. But as you are new here a hint: Remove your custom decoding initializer as it is not needed and most likely throws this error. – burnsi Aug 20 '22 at 20:22
  • sorry, will know that from now on. Thanks for your hint – Shum Aug 20 '22 at 20:23