swiftobjective-c

How can I encode/decode a Swift dictionary with Obj-C objects as keys to and from JSON?


I have a Dictionary<ObjcType, SwiftType> that I have to serialize to and deserialize from JSON. SwiftType conforms to Codable. ObjcType is a class written in Objective-C (used by one of the libraries I heavily depend on, so cannot change it to be in Swift). All properties of ObjcType are either NSString or BOOL.

I did some search and added these functions to ObjcType, to give it some semblance of serialization:

-(NSString*)GetJSON{
    NSError *writeError = nil;

    NSData *jsonData = [NSJSONSerialization dataWithJSONObject:self options:NSJSONWritingPrettyPrinted error:&writeError];

    NSString *jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];

    return jsonString;
}

- (instancetype)initWithObject:(NSString *)jsonString {
    self = [super init];
    if (self != nil)
    {
        NSData* jsonData = [jsonString dataUsingEncoding:NSUTF8StringEncoding];
        
        NSError *error = nil;
        NSDictionary  *object = [NSJSONSerialization
                                 JSONObjectWithData:jsonData
                                 options:0
                                 error:&error];
        for (NSString *dictionaryKey in object) {
            self.field1 = [[object valueForKey:dictionaryKey] objectForKey:@"field1"];
            self.field2 = [[object valueForKey:dictionaryKey] objectForKey:@"field2"];
            self.field3 = [[object valueForKey:dictionaryKey] objectForKey:@"field3"];
        }
    }
    return self;
}

So now my ObjcType can create its own text to put into that JSON. But I cannot figure out how to make a wrapper that will encode\decode this dictionary. Any help?

P.S. even better is it could work with OrderedDictionary from OrderedCollections module instead of normal Dictionary.


Solution

  • You can write a wrapper like this:

    @propertyWrapper
    struct Wrapper: Codable {
        struct StringCodingKey: CodingKey {
            var intValue: Int? { nil }
            let stringValue: String
            
            init?(intValue: Int) { return nil }
            init(stringValue value: String) { stringValue = value }
        }
        
        var wrappedValue: [ObjcType: SwiftType]
        
        func encode(to encoder: any Encoder) throws {
            var container = encoder.container(keyedBy: StringCodingKey.self)
            for (key, value) in wrappedValue {
                // assuming there is property called 'jsonString'
                let keyString = key.jsonString
                try container.encode(value, forKey: StringCodingKey(stringValue: keyString))
            }
        }
        
        init(wrappedValue: [ObjcType : SwiftType]) {
            self.wrappedValue = wrappedValue
        }
        
        init(from decoder: any Decoder) throws {
            var wrapped = [ObjcType: SwiftType]()
            let container = try decoder.container(keyedBy: StringCodingKey.self)
            for key in container.allKeys {
                // assuming such an initialiser exists
                let objcKey = ObjcType(json: key.stringValue)
                wrapped[objcKey] = try container.decode(SwiftType.self, forKey: key)
            }
            self.wrappedValue = wrapped
        }
    }
    

    Usage:

    // decoding
    JSONDecoder().decode(Wrapper.self, from: someJson).wrappedValue
    
    // encoding
    JSONEncoder().encode(Wrapper(wrapped: yourActualDictionary))
    

    You can also use it as a property wrapper in other Codable types:

    struct SomeOtherCodableType: Codable {
        @Wrapper var dict: [ObjcType: SwiftType]
    }