A bit late to the party, but I think this is great opportunity for JSONEncoder and JSONSerialization.
The accepted answer does touch on this, this solution saves us calling JSONSerialization every time we access a key, but same idea!
extension Encodable {
    /// Encode into JSON and return `Data`
    func jsonData() throws -> Data {
        let encoder = JSONEncoder()
        encoder.outputFormatting = .prettyPrinted
        encoder.dateEncodingStrategy = .iso8601
        return try encoder.encode(self)
    }
}
You can then use JSONSerialization to create a Dictionary if the Encodable should be represented as an object in JSON (e.g. Swift Array would be a JSON array)
Here's an example:
struct Car: Encodable {
    var name: String
    var numberOfDoors: Int
    var cost: Double
    var isCompanyCar: Bool
    var datePurchased: Date
    var ownerName: String? // Optional
}
let car = Car(
    name: "Mazda 2",
    numberOfDoors: 5,
    cost: 1234.56,
    isCompanyCar: true,
    datePurchased: Date(),
    ownerName: nil
)
let jsonData = try car.jsonData()
// To get dictionary from `Data`
let json = try JSONSerialization.jsonObject(with: jsonData, options: [])
guard let dictionary = json as? [String : Any] else {
    return
}
// Use dictionary
guard let jsonString = String(data: jsonData, encoding: .utf8) else {
    return
}
// Print jsonString
print(jsonString)
Output:
{
  "numberOfDoors" : 5,
  "datePurchased" : "2020-03-04T16:04:13Z",
  "name" : "Mazda 2",
  "cost" : 1234.5599999999999,
  "isCompanyCar" : true
}