1

Very simply, I have this:

public struct SomeAPI: Codable {
    public let a: String
    public let b: String
    public let c: String
    public let d: String
}

and I of course do this ...

let s = try? JSONDecoder().decode(SomeAPI.self, from: data)

Imagine I'm decoding a few hundred of those, perhaps in an array.

Very simply, after each one is decoded, I want to run some code.

So conceptually something like ..

public struct SomeAPI: Codable {
    init-something() {
       super.something = magic
       print("I just decoded one of the thingies! a is \(a)")
    }
    public let a: String
    public let b: String
    public let c: String
    public let d: String
}

I'm afraid I have absolutely no idea how to do that, having searched much. How is it done? It seems a shame to do it manually as a separate step.

Fattie
  • 27,874
  • 70
  • 431
  • 719
  • 3
    Have you looked into implementing [`init(from:)`](https://developer.apple.com/documentation/swift/decodable/init(from:)) yourself for the `SomeAPI` type? It sounds like that's what you're looking for — you can manually decode all of the properties by implementing that method, then add whatever code you want. – Itai Ferber Sep 06 '22 at 23:44
  • 1
    It would help to know more specifically what you want to do after decoding. I very often solve this sort of problem with a calculated or lazy property of the struct. Or perhaps you want a "reducer" that processes the decoded struct. Please provide more detail. – matt Sep 07 '22 at 00:33
  • cheers @ItaiFerber ! Yes, I essentially want someone to tell me how to do that :) I want it to automatically decode (as usual) and then i can add a print statement (or whatever I wish). {It's quite common on this site to ask "How the hell do I subclass {ie, do the initialization for} a UICollectionView ..." or such, don't you think? Because those can be tricky and non-intuitive. This is essentially such a question ...} – Fattie Sep 07 '22 at 11:58
  • cheers @matt I want it to automatically decode (as it usually does) and then I can run any code I wish, such as a print statement, a reducer as you say, a log entry, perhaps start another process, queue a pub-nub type request or a number of other scenarios. (lazy properties are indeed very useful in Decodables, but, a huge negative is that every time you make a new reference it is calculated again of course). So yes I just want to know how to "subclass the decoding process", call "super .. the usual process!" and then run my own code. – Fattie Sep 07 '22 at 12:09

1 Answers1

1

The Encoding and Decoding Custom Types documentation is a great starting point for understanding how to implement the Encodable and Decodable method requirements to add to your types — and specifically, the Encoding and Decoding Manually section shows how to override the compiler-synthesized implementation of these methods to provide your own.

It's important to understand how the automated approach to Codable works, though. In a nutshell: for a type that adopts Encodable/Decodable, the compiler checks whether it's provided its own implementation of the associated method, and if not, attempts to fill it in on its own. There's no magic there: it fills in its internal state (manipulating the AST) with an implementation as if you had written it yourself in code.

For your example SomeAPI type, the generated code inside of the compiler looks exactly as if you had typed the following in yourself:

struct SomeAPI: Codable {
    let a: String
    let b: String
    let c: String
    let d: String

    private enum CodingKeys: String, CodingKey {
        case a, b, c, d
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        a = try container.decode(String.self, forKey: .a)
        b = try container.decode(String.self, forKey: .b)
        c = try container.decode(String.self, forKey: .c)
        d = try container.decode(String.self, forKey: .d)
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(a, forKey: .a)
        try container.encode(b, forKey: .b)
        try container.encode(c, forKey: .c)
        try container.encode(d, forKey: .d)
    }
}

This only happens if you haven't implemented the protocol requirement yourself, though: if you have, the compiler leaves the existing implementation alone, assuming you've done exactly what you want.

This last point is relevant to your use-case, because it sounds like what you'd like to do is implement this type like

struct SomeAPI: Codable {
    let a: String
    let b: String
    let c: String
    let d: String

    init(from decoder: Decoder) throws {
        somehowCallTheCompilerSynthesizedImplementationOfThisMethod()
        /* perform some other stuff */
    }
}

The issue is that when you provide your own init(from:) implementation, the compiler won't synthesize anything for your type, so there's no "default" implementation to call; unfortunately, it's an all-or-nothing deal.

To do this, then, you can either:

  1. Implement init(from:) on your types by implementing the full method, then adding whatever code you'd like, or

  2. Inherit the compiler-synthesized implementation in a bit of a roundabout way: if you turn SomeAPI into a class, you can allow it to receive the default synthesized implementation, then override the synthesized implementation to do what you want (demonstrated here by moving its contents into a "dummy" parent type, then keep using SomeAPI as a subclass):

    class _SomeAPI: Codable {
        let a: String
        let b: String
        let c: String
        let d: String
    }
    
    class SomeAPI: _SomeAPI {
        required init(from decoder: Decoder) throws {
            try super.init(from: decoder)
            /* do whatever */
        }
    }
    

    The downside of this approach, of course, is that this changes the semantics of the SomeAPI type, which may very well not be what you want

Approach (1) is likely a good starting approach as it keeps the semantics of this type the same, and there are tools out there that can help automatically output code for a given Swift type (e.g. Sourcery) if this gets repetitive.

Itai Ferber
  • 28,308
  • 5
  • 77
  • 83
  • AHHHHHH! so you CAN NOT automatically get the (lets put it this way) four lines of code which decode a, b, c and d. You'd have to manually enter those. That's highly dangerous and ungood, since fields are commonly added/removed in the exact milieu of decoding json. – Fattie Sep 07 '22 at 16:20
  • AHHHHHH! so it CAN be done, but only if you change to a class? INCREDIBLE KNOWLEDGE – Fattie Sep 07 '22 at 16:21