問題情境

如何產生一個 Struct 所對應資料的 Schema 呢?

一開始我們有個資料結構如下,有包含名稱、年紀、標籤清單、網站資訊、訊息列表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
"name": "pink",
"age": 18,
"tags": ["iOS", "Apple", "Swift"],
"webInfo": {
"title": "pinkpika github",
"url": "https://github.com/pinkpika"
},
"posts": [
{
"time": 1671321786,
"text": "今天是個好天氣。"
},
{
"time": 1671436986,
"text": "吃到一間好吃的蛋糕店!"
}
]
}

可以很直覺地建立一個叫 Member 的 Struct,並且 conform Codable 方便解析該 json 資料。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct Member: Codable{
let name: String
let age: Int
let tags: [String]
let webInfo: WebInfo
let posts: [Post]
}
struct WebInfo: Codable{
let title: String
let url: String
}
struct Post: Codable{
let time: Double
let text: String
}

此時我們不僅需要解析上面的 json,同時我們也想要自動產生該資料的 Schema,如同下面的格式。

  • key: 欄位名稱
  • des: 中文敘述 ( 需要給 Server 類似註解的東西 )
  • type: 資料類型 ( 有 string、number、object、arrayObject、arrayNumber、arrayString )
  • object: 物件定義 ( 資料類型 = object 或是 arrayObject 才有 )
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
[
{
"key": "name",
"des": "姓名",
"type": "string"
},
{
"key": "age",
"des": "年紀",
"type": "number"
},
{
"key": "tags",
"des": "標籤清單",
"type": "arrayString"
},
{
"key": "webInfo",
"des": "網站訊息",
"type": "object"
"object": [
{
"key": "title",
"des": "標題",
"type": "string"
},
{
"key": "url",
"des": "網址",
"type": "string"
}
]
},
{
"key": "posts",
"des": "訊息列表",
"type": "arrayObject",
"object": [
{
"key": "time",
"des": "時間",
"type": "number"
},
{
"key": "text",
"des": "文字",
"type": "string"
}
]
}
]

解法討論

使用 Mirror + PropertyWrapper 的組合技

想法提示:

  1. 自動產生 Schema 可使用 Mirror
  2. 資料 Schema 的注解可使用 PropertyWrapper

先定義一個 DataSchema,它包含 key、des、type、object,所有 Schema 需要的資料。

再定義一個 protocol 叫做 DataModel,並且實作一個 getDataSchema 的功能,內部使用 Mirror 反射出屬性的”名稱”和”類型”,然後依照不同的屬性類型轉換成不同的 DataSchema。

如果該屬性不是原生類型,而是自定義物件,則需要使用 genericType as? DataModel.Type 轉換成 DataModel,並使用遞迴的方式呼叫 getDataSchema 取得內部屬性。

如果是自定義物件的陣列,則需要使用 let collectionType = genericType as? any CollectionProtocol.TypecollectionType.getElementType() 取出藏在陣列裡的元素,然後一樣呼叫 getDataSchema 取得內部屬性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
struct DataSchema: Codable{
enum ParaType: String, Codable{
case string
case number
case object
case arrayObject
case arrayNumber
case arrayString
}
let key: String
let des: String
let type: ParaType
let object: [DataSchema]?
}
protocol DataModel{
init()
func getDataSchema() -> [DataSchema]
}
extension DataModel{
func getDataSchema() -> [DataSchema]{
var output: [DataSchema] = []
let mirror = Mirror(reflecting: self)
for case let (label?, value) in mirror.children {

guard let generic = value as? any GenericReflectable else { continue }

let genericType = generic.getRealValueType().self
let key = getFixName(label)
let des = generic.getDescription()

if genericType is Int.Type || genericType is Double.Type {
output.append(DataSchema(key: key, des: des, type: .number, object: nil))
} else if genericType is String.Type {
output.append(DataSchema(key: key, des: des, type: .string, object: nil))
} else if genericType is [Int].Type || genericType is [Double].Type {
output.append(DataSchema(key: key, des: des, type: .arrayNumber, object: nil))
} else if genericType is [String].Type {
output.append(DataSchema(key: key, des: des, type: .arrayString, object: nil))
} else if let configModelType = genericType as? DataModel.Type {
let object = configModelType.init().getDataSchema()
output.append(DataSchema(key: key, des: des, type: .object, object: object))
} else if let collectionType = genericType as? any CollectionProtocol.Type ,
let configModelType = collectionType.getElementType().self as? DataModel.Type{
let object = configModelType.init().getDataSchema()
output.append(DataSchema(key: key, des: des, type: .arrayObject, object: object))
}
}
return output
}
private func getFixName(_ name: String) -> String {
return name.replacingOccurrences(of: "_", with: "")
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
struct Member: Codable, DataModel{
@DataProperty(description: "姓名")
var name: String?
@DataProperty(description: "年紀")
var age: Int?
@DataProperty(description: "標籤清單")
var tags: [String]?
@DataProperty(description: "網站訊息")
var webInfo: WebInfo?
@DataProperty(description: "訊息列表")
var posts: [Post]?
}
struct WebInfo: Codable, DataModel{
@DataProperty(description: "標題")
var title: String?
@DataProperty(description: "網址")
var url: String?
}
struct Post: Codable, DataModel{
@DataProperty(description: "時間")
var time: Double?
@DataProperty(description: "文字")
var text: String?
}

遇到問題:

  1. 中間有遇到泛型 propertyWrapper 無法取得原始類型的問題 => 實作 GenericReflectable 來解決
  2. 中間有遇到 Array 無法取得 Element 的問題 => 實作 CollectionProtocol 來解決
  3. 中間有遇到 DataProperty Codable 需要改寫的問題,只要數值無需註解 => 實作 Codable decoder 來解決
  4. 中間有遇到 DataProperty 可能為 nil 的問題 => 實作 KeyedDecodingContainer 來解決
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
/// 泛型反射出需要的資料
protocol GenericReflectable{
associatedtype RealValueType
func getDescription() -> String
func getRealValueType() -> RealValueType.Type?
}
/// DataProperty ( DataModel 的欄位 )
@propertyWrapper
struct DataProperty<T: Codable>: Codable, GenericReflectable{
typealias RealValueType = T
var wrappedValue: T?
var description: String = ""
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
wrappedValue = try? container.decode(T.self)
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(wrappedValue)
}
init(description: String) {
self.description = description
}
init(wrappedValue: T?) {
self.wrappedValue = wrappedValue
}
func getDescription() -> String {
return description
}
func getRealValueType() -> RealValueType.Type?{
return T.self
}
}
extension KeyedDecodingContainer {
func decode<T: Codable>(_ type: DataProperty<T>.Type, forKey key: Key) throws -> DataProperty<T> {
return try decodeIfPresent(type, forKey: key) ?? .init(wrappedValue: nil)
}
}
protocol CollectionProtocol {
static func getElementType() -> Any.Type
}
extension Array: CollectionProtocol {
static func getElementType() -> Any.Type {
return Element.self
}
}

結果測試

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
print("🟢 DataSchema =============================================")
let dataSchema = Member().getDataSchema()
if let data = try? JSONEncoder().encode(dataSchema) {
print("output:", String(decoding: data, as: UTF8.self)) // <----- 正確顯示 DataSchema
}

print("🟢 DataSchema Decoder =============================================")
let jsonString = """
{
"name": "pink",
"age": 18,
"tags": ["iOS", "Apple", "Swift"],
"webInfo": {
"title": "pinkpika github",
"url": "https://github.com/pinkpika"
},
"posts": [
{
"time": 1671321786,
"text": "今天是個好天氣。"
},
{
"time": 1671436986,
"text": "吃到一間好吃的蛋糕店!"
}
]
}
"""
let jsonData = Data(jsonString.utf8)
do {
let model = try JSONDecoder().decode(Member.self, from: jsonData) // <----- 正確解析 Json 物件
print(model.name)
print(model.age)
print(model.tags)
print(model.webInfo)
print(model.posts)
} catch {
print(error)
}