struct Classroom: Codable {
let teacher: String
let id: Int
let status: Bool
init(from decoder: Decoder) throws {
...
...
}
}
Now I need a way to create Classroom
instance with a simple String
{ "classroom": "Test" }//received from API
let classRoom = ClassRoom(teacher: "Test", id: 0, status: true)
Now I need to add a secondary decoder
method which can create this classroom
instance using the "classroom": "Test"
data. The "Test" value should be used as value for "teacher" and other properties should contain default values.
I know I can decode the String
value and create a new initializer. Is there a way to directly decode String
to this model object?
CodePudding user response:
if i understand well, i assume you have a bad json format like below
[
{
"teacher":"test",
"id":5,
"status":true
},
{
"classroom":"Test"
}
]
And you want to decode both objects, you can do the following
let data = """
[
{
"teacher": "test",
"id": 5,
"status": true
},
{
"classroom": "Test"
}
]
""".data(using: .utf8)!
struct Classroom: Codable {
let teacher: String
let id: Int
let status: Bool
private enum CodingKeys: CodingKey {
case teacher, id, status
}
private enum SecCodingKeys: CodingKey {
case classroom
}
init(from decoder: Decoder) throws {
let value = try decoder.container(keyedBy: CodingKeys.self)
let secValue = try decoder.container(keyedBy: SecCodingKeys.self)
let teacher_1 = try value.decodeIfPresent(String.self, forKey: .teacher)
let teacher_2 = try secValue.decodeIfPresent(String.self, forKey: .classroom)
teacher = teacher_1 ?? teacher_2 ?? ""
id = try value.decodeIfPresent(Int.self, forKey: .id) ?? 0
status = try value.decodeIfPresent(Bool.self, forKey: .status) ?? false
}
}
do {
let rooms = try JSONDecoder().decode([Classroom].self, from: data)
print(rooms.map(\.teacher))
} catch {
print(error)
}
and the result,
["test", "Test"]
CodePudding user response:
Decode the second, nested case, as another type
struct SimpleClassroom: Decodable {
let classroom: String
}
and then have a computed property for mapping to the original type with default values
extension SimpleClassroom {
var classroomValue: Classroom {
Classroom(teacher: classroom, id: 0, status: false)
}
}
CodePudding user response:
If "Test"
is a valid description of classroom, and you want to go ahead and create the classroom, then you have a number of options.
If you know classrooms from a given API endpoint will always be in this string format, you can use the decoder's context dictionary to tell it up front which strategy to use to decode the classroom. If sometimes a classroom is a properly formed dictionary, and sometimes it's just a string, and you want to proceed either way, then you have to handle that case in the init(from:)
.
Either way you're looking at a custom init method. The second case, where you handle both types, would look like this:
init(from decoder: Decoder) throws {
// Do we have a single-value container?
do {
let container = try decoder.singleValueContainer()
let string = try container.decode(String.self)
self.teacher = string
self.id = 0
self.status = true
return
} catch {
// OK, it was a dictionary
}
let container = try decoder.container(keyedBy: CodingKeys.self)
self.teacher = try container.decode(String.self, forKey: .teacher)
self.id = try container.decode(Int.self, forKey: .id)
self.status = try container.decode(Bool.self, forKey: .status)
}
Given this made-up, horrible JSON:
[
"Test",
{ "teacher": "Mr Chips", "id": 0, "status": true }
]
let rooms = try JSONDecoder().decode([Classroom].self, from: data)
Gives you two valid Classroom
types in an array.