Search code examples
jsonswift4decoder

JSON Decoding into variant model Swift4


I have two JSON requests. There is a header and a different body depending of MessageCategory in the header. How do I decode this in ONE playground.

The first playground is:

import Cocoa

var str = "SimplePaymentJSON playground"


struct Request :Decodable {
    var SaleToPOIRequest : SaleToPOIRequestStr
}

struct MessageHeaderStr : Decodable {

    var MessageClass : String
    var MessageCategory : String
    var MessageType : String
    var ServiceID : String
    var SaleID : String
    var POIID : String
}

struct PaymentRequestStr :  Decodable {
    var SaleData : SaleData
    var PaymentTransaction : PaymentTransaction
    var PaymentData : PaymentData
}

struct SaleToPOIRequestStr :  Decodable {
    var MessageHeader : MessageHeaderStr
    var PaymentRequest : PaymentRequestStr

}

struct SaleData : Decodable {
    var SaleTransactionID : SaleTransactionID
}

struct PaymentTransaction : Decodable {

    var AmountsReq : AmountsReq
    var TransactionConditions: TransactionConditions

}

struct SaleTransactionID: Decodable {
    var TransactionID : String
    var TimeStamp : String
}

struct AmountsReq: Decodable {
    var Currency : String
    var RequestedAmount : String
}

struct TransactionConditions: Decodable {
    var LoyaltyHandling : String
}

struct PaymentData: Decodable {
    var PaymentType : String
}


let json = """

{
  "SaleToPOIRequest" : {
    "MessageHeader" : {
      "MessageClass": "Service",
      "MessageCategory": "Payment",
      "MessageType": "Request",
      "ServiceID": "642",
      "SaleID": "SaleTermA",
      "POIID": "POITerm1"
    },
    "PaymentRequest": {
      "SaleData": {
        "SaleTransactionID": {
          "TransactionID": "579",
          "TimeStamp": "2009-03-10T23:08:42.4+01:00"
        }
      },
      "PaymentTransaction": {
        "AmountsReq": {
          "Currency": "EUR",
          "RequestedAmount": "104.11"
        },
        "TransactionConditions": { "LoyaltyHandling": "Forbidden" }
      },
      "PaymentData": { "PaymentType": "Normal" }
    }
  }
}

""".data(using: .utf8)!

// let customer = try! JSONDecoder().decode(Customer.self, from: json)
// print(customer)
do {

    let paymentRequest = try JSONDecoder().decode(Request.self, from: json )

    print(paymentRequest)
    print(paymentRequest.SaleToPOIRequest.MessageHeader.MessageType)
    print(paymentRequest.SaleToPOIRequest.PaymentRequest.PaymentTransaction.AmountsReq.Currency)
    print(paymentRequest.SaleToPOIRequest.PaymentRequest.PaymentTransaction.AmountsReq.RequestedAmount)

}

catch let jsonErr {

    print("Error decoding Json", jsonErr)

}

The other playground is:

import Cocoa

var str = "LoginJSON playground"


struct Request : Decodable {
    var SaleToPOIRequest : SaleToPOIRequestStr
}

struct MessageHeaderStr : Decodable {

    var MessageClass : String
    var MessageCategory : String
    var MessageType : String
    var ServiceID : String
    var SaleID : String
    var POIID : String
}

struct LoginRequestStr :  Decodable {
    var OperatorLanguage : String
    var OperatorID : String
    var ShiftNumber : String
    var POISerialNumber : String
    var DateTime : String
    var SaleSoftware : SaleSoftwareStr
    var SaleTerminalData : SaleTerminalDataStr
}

struct SaleToPOIRequestStr :  Decodable {
    var MessageHeader : MessageHeaderStr
    var LoginRequest : LoginRequestStr

}

struct SaleSoftwareStr :  Decodable {
    var ProviderIdentification : String
    var ApplicationName : String
    var SoftwareVersion : String
    var CertificationCode : String
}

struct SaleProfileStr : Decodable {
    var GenericProfile : String
    var ServiceProfiles : String
}

struct SaleTerminalDataStr :  Decodable {

    var TerminalEnvironment : String
    var SaleCapabilities : String
    var SaleProfile : SaleProfileStr
}



let json = """

{
  "SaleToPOIRequest": {
    "MessageHeader": {
      "ProtocolVersion": "3.0",
      "MessageClass": "Service",
      "MessageCategory": "Login",
      "MessageType": "Request",
      "ServiceID": "498",
      "SaleID": "SaleTermA",
      "POIID": "POITerm1"
    },
    "LoginRequest": {
      "OperatorLanguage": "de",
      "OperatorID": "Cashier16",
      "ShiftNumber": "2",
      "POISerialNumber": "78910AA46010005",
      "DateTime": "2015-03-08T09:13:51.0+01:00",
      "SaleSoftware": {
        "ProviderIdentification": "PointOfSaleCo",
        "ApplicationName": "SaleSys",
        "SoftwareVersion": "01.98.01",
        "CertificationCode": "ECTS2PS001"
      },
      "SaleTerminalData": {
        "TerminalEnvironment": "Attended",
        "SaleCapabilities": "PrinterReceipt CashierStatus CashierError CashierDisplay CashierInput",
        "SaleProfile": {
          "GenericProfile": "Extended",
          "ServiceProfiles": "Loyalty PIN CardReader"
        }
      }
    }
  }
}

""".data(using: .utf8)!

// let customer = try! JSONDecoder().decode(Customer.self, from: json)
// print(customer)
do {

    let loginRequest = try JSONDecoder().decode(Request.self, from: json )

    print(loginRequest)
    print(loginRequest.SaleToPOIRequest.MessageHeader.ServiceID)
    print(loginRequest.SaleToPOIRequest.LoginRequest.DateTime)
    print(loginRequest.SaleToPOIRequest.LoginRequest.SaleSoftware.CertificationCode)
    print(loginRequest.SaleToPOIRequest.LoginRequest.SaleTerminalData.SaleProfile.GenericProfile)

}

catch let jsonErr {

    print("Error decoding Json", jsonErr)

}

How could I decode depending on MessageCategory in the MessageHeader?

Is there something link a UNION?

Kind Regards


Solution

  • As far as I can see in your case, SaleToPOIRequest contains a property of type PaymentRequestStr or LoginRequestStr. I would recommend you to declare enum with associated values to store an instance of PaymentRequestStr or LoginRequestStr. Check this code snippet:

    enum LoginOrPaymentRequest: Decodable {
    
        case login(LoginRequestStr)
        case payment(PaymentRequestStr)
        case unknown
    
        private enum CodingKeys: String, CodingKey {
    
            case LoginRequest
            case PaymentRequest
        }
    
        init(from decoder: Decoder) throws {
            let container = try decoder.container(keyedBy: CodingKeys.self)
            if let loginRequest = try container.decodeIfPresent(LoginRequestStr.self, forKey: .LoginRequest) {
                self = .login(loginRequest)
            } else if let paymentRequest = try container.decodeIfPresent(PaymentRequestStr.self, forKey: .PaymentRequest) {
                self = .payment(paymentRequest)
            } else {
                self = .unknown
            }
        }
    }
    

    Also change the implementation of SaleToPOIRequest, add a property of type LoginOrPaymentRequest:

    struct SaleToPOIRequest: Decodable {
    
        let messageHeader: MessageHeaderStr?
        let loginOrPaymentRequest: LoginOrPaymentRequest
    
        private enum CodingKeys: String, CodingKey {
    
            case MessageHeader
        }
    
        private init(messageHeader: MessageHeaderStr?,
                     loginOrPaymentRequest: LoginOrPaymentRequest) {
            self.messageHeader = messageHeader
            self.loginOrPaymentRequest = loginOrPaymentRequest
        }
    
        init(from decoder: Decoder) throws {
            let container = try decoder.container(keyedBy: CodingKeys.self)
            self.init(messageHeader: try container.decode(MessageHeaderStr.self, forKey: .MessageHeader),
                      loginOrPaymentRequest: try LoginOrPaymentRequest(from: decoder))
        }
    }
    

    This structure contains the information about an instance of SaleToPOIRequest:

    struct DataResponse: Decodable {
    
        let saleToPOIRequest: SaleToPOIRequest?
    
        private enum CodingKeys: String, CodingKey {
            case SaleToPOIRequest
        }
    
        private init(saleToPOIRequest: SaleToPOIRequest?) {
            self.saleToPOIRequest = saleToPOIRequest
        }
    
        init(from decoder: Decoder) throws {
            let container = try decoder.container(keyedBy: CodingKeys.self)
            self.init(saleToPOIRequest: try container.decode(SaleToPOIRequest.self, forKey: .SaleToPOIRequest))
        }
    }
    

    How to test it:

    let dataResponse = try JSONDecoder().decode(DataResponse.self, from: json)
    print(dataResponse.saleToPOIRequest?.messageHeader?.POIID)
    if let loginOrPaymentRequest = dataResponse.saleToPOIRequest?.loginOrPaymentRequest {
        if case .login(let loginRequest) = loginOrPaymentRequest {
            print(loginRequest.OperatorLanguage)
        } else if case .payment(let paymentRequest) = loginOrPaymentRequest {
            print(paymentRequest.PaymentData.PaymentType)
        }
    }
    

    I remember that you mentioned that you need to check the value of MessageCategory. In such case add this function to LoginOrPaymentRequest:

    init(from decoder: Decoder, messageCategory: String) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        if let loginRequest = try container.decodeIfPresent(LoginRequestStr.self, forKey: .LoginRequest), messageCategory == "Login" {
            self = .login(loginRequest)
        } else if let paymentRequest = try container.decodeIfPresent(PaymentRequestStr.self, forKey: .PaymentRequest), messageCategory == "Payment" {
            self = .payment(paymentRequest)
        } else {
            self = .unknown
        }
    }
    

    And modify init of SaleToPOIRequest:

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        let messageHeader = try container.decode(MessageHeaderStr.self, forKey: .MessageHeader)
        let loginOrPaymentRequest = try LoginOrPaymentRequest(from: decoder,
                                                              messageCategory: messageHeader.MessageCategory)
        self.init(messageHeader: messageHeader,
                  loginOrPaymentRequest: loginOrPaymentRequest)
    }