Inheriting from a Codable class

In the following code, I get the error that SuperClass has no initialisers. However the compiler is able to automatically generate code to conform to the Cod able protocols, it doesn't seem to do this with classes that inherit from a class that is Codable.

class BaseClass: Codable {
let foo: String
}

class SuperClass: BaseClass {
let bar: Int

// required init(from decoder: Decoder) throws {
// let container = try decoder.container(keyedBy: CodingKeys.self)
// bar = try container.decode(Int.self, forKey: .bar)
// try super.init(from: decoder)
// }
//
// override func encode(to encoder: Encoder) throws {
// var container = encoder.container(keyedBy: CodingKeys.self)
// try container.encode(bar, forKey: .bar)
// try super.encode(to: encoder)
// }
//
// enum CodingKeys: String, CodingKey {
// case bar
// }
}

let s = try? JSONDecoder().decode(SuperClass.self, from: "{ "foo": "Hello", "bar": 2 }".data(using: .utf8)!)

String.init(data: try! JSONEncoder().encode(s), encoding: .utf8)

The commented code shows the code that is required to make it work, and it's quite clearly capable of being auto generated by the compiler

is this another half implemented feature and we can expect this to work or is there some obscure reason why automatic conformance doesn't work here, even though it would be useful to have this a default which the developer can implement themselves if needed?

The compiler is telling SuperClass has no initializer, because SuperClass has no initializer. Either define an initializer or give bar a default value.

In addition, SuperClass needs to conform to Codable. It doesn't automatically conform because BaseClass conforms.

This is not true here — any class which inherits from a class that adopts a protocol inherits that protocol conformance; that inheritance may conflict with other requirements on the class, though. This is actually exactly what leads to this behavior: SuperClass inherits BaseClass’s Codable implementation directly as it cannot yet synthesize its own.

In any inheritance situation, if a subclass adds new properties which do not have default values, it cannot inherit its superclass’s initializers since those would not initialize said properties. One would need to add an initializer which explicitly initializes SuperClass.bar, i.e., init(from:).

Essentially the following is happening:

class Foo { var a: Int; init() { a = 3 } }
class Bar : Foo { var b: String } // Class ‘Bar’ has no initializers

The reason the compiler cannot synthesize an implementation on behalf of SuperClass is mostly due to two things:

  1. Some amount of implementation detail (which is not a good answer)

  2. There’s currently no way to indicate in any given class whether a subclass wants to inherit or re-synthesize conformance. This is a somewhat unique situation: in traditional inheritance, there are only two options — either you provide an overridden implementation of a method, or you don’t. Synthesis adds a third option in allowing neither you nor your superclass to provide that code. We would need new syntax to explicitly specify that you’d like to synthesize over the other two options, since doing nothing already indicates “inherit”.

    There are thoughts about disambiguating the situation (e.g. redeclaring conformance to a synthesized protocol could indicate you’d like to re-synthesize instead of inherit, i.e. class SuperClass : BaseClass, Codable {...} which is currently disallowed due to being redundant), but nothing concrete yet.

There’s some amount of design work that needs to happen here that we plan on doing.

(See also SR-4772 and SR-7210)

3 Likes

At least with Decodable, there is no option of choosing to inherit the implementation as you can't inherit initialisers like that.

Tbh, I think the whole auto synthesised conformance system is ugly. I'm surprised it was allowed to be implemented based on some of the other things that have been refused based on them not being "swifty" enough. Also the fact that gyb.py needs to be used anywhere (not just for Codable), and that synthesised conformance has to be implemented in the compiler shows that Swift is not as capable as we need it to be.

  1. Developers should be able to create protocols that have synthesised conformance, not just people who maintain the compiler.

  2. it should be obvious which protocols synthesise conformance. eg class Foo : @synthesize Codable { } in the same manner of @autoclosure. This would also mean that leaving off that attribute requires you to implement it yourself, solving this problem.

2 Likes

What do you mean? Inheritance is the only option here — synthesis does not happen for subclasses, which is the issue. Or am I misunderstanding the assertion?

This is a non-trivial addition and change to the language, though one that I would readily welcome! There are languages which do offer the power to modify the AST at compile-time in userland code (e.g. Rust and Nim), which would be great. Getting to the point where we could offer that with a syntax that's easy to express and understand... is something else altogether.

Hygienic macros are on the roadmap for Swift eventually, and there has been a lot of discussion around them and the need for them, but we're not there yet.

This is indeed one solution to the problem. Until now we've approached synthesis as something implicit and automatic, but it's not necessarily too late to change. Rust, for instance, has #[derive(...)] to request and indicate synthesis, and that's one route we could go down too.

One difficulty here is source compatibility — a lot of code out there already requires this implicit synthesis, and we'd need a migration strategy to bring all of that code forward. You could imagine, for instance, that Swift 6 would start warning and providing fix-its on implicit synthesis, and Swift 7/8 would require explicit synthesis.

[As an aside, there are other forms of synthesis in the compiler which are not so clear; for instance, the compiler has long synthesized RawRepresentable conformance on enums which are backed by primitive types even if those types didn't request to be RawRepresentable, and this is something that I think we'd like to keep indefinitely.]

If this is something that we view is the better way forward, then a pitch and gathering ideas around this would be an excellent start.

You said there were 3 options, inherit, provide an implementation, or synthesis. But for Decodable which is a initialiser, there are only two options provide an implementation or sythesis as you can't inherit the implementation of initialisers. (though this isn't really relevant to the larger topic as it's specific to Decodable)

I think it would be great to be able to go all the way and say download a file (eg swagger file), or load a file like a graphql query, and generate an swift API from it, all from within Swift using the APIs (to download and load files) that swift developers already know and use. Even just a swift version of gyb.py with built in support in the compiler (eg .tswift files are found by the compiler and internally run through the equivalent of gyb) would work.

The same migration strategy as the @escaping for closure parameters would work here I think.

I think primitives are enough of a special case already, and this wouldn't have to be perfectly uniform there are edge cases to everything.

This is not true — classes can definitely inherit initializers. See the "Automatic Initializer Inheritance" section from the Swift language guide for more info on that. To clarify this case specifically here:

  • For a base class conforming to Decodable (e.g. your BaseClass), you have two options: manually implement Decodable, or allow the compiler to synthesize it for you, if possible
  • For a class adopting Decodable inheriting from a class which does not (e.g. Bar in class Foo {} class Bar : Foo, Decodable {}) you have the same two options: manually implement Decodable, or allow the compiler to synthesize
  • For a subclass of a class already adopting Decodable (e.g. your SuperClass), you have to options: override your superclass's implementation by offering your own, or inheriting it, if possible. There is no way for the compiler to synthesize an implementation here for you

To show off this last case, let's implement BaseClass and SuperClass manually with the same code that we'd expect compiler to otherwise generate:

import Foundation

class BaseClass : Decodable {
    var foo: String
    init(foo: String) { self.foo = foo }

    private enum CodingKeys: String, CodingKey {
        case foo
    }

    required init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        foo = try container.decode(String.self, forKey: .foo)
    }
}

class SuperClass : BaseClass {
    var bar: Int
    init(foo: String, bar: Int) {
        self.bar = bar
        super.init(foo: foo)
    }

    private enum CodingKeys: String, CodingKey {
        case bar
    }

    required init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        bar = try container.decode(Int.self, forKey: .bar)
        try super.init(from: container.superDecoder())
    }
}

let decoder = JSONDecoder()
let json = """
{
    "bar": 42,
    "super": { "foo": "Hello, world!" }
}
""".data(using: .utf8)!

let superClass = try! decoder.decode(SuperClass.self, from: json)
print(superClass.foo, superClass.bar) // Hello, world! 42

This prints out "Hello, world! 42", as we might expect. Now, let's try removing BaseClass.init(from:) to allow the compiler to synthesize the implementation:

class BaseClass : Decodable {
    var foo: String
    init(foo: String) { self.foo = foo }
}

// ...

print(superClass.foo, superClass.bar) // => Hello, world! 42

So far, no change in behavior — great! Now, let's try the same with SuperClass:

class BaseClass : Decodable {
    var foo: String
    init(foo: String) { self.foo = foo }
}

class SuperClass : BaseClass {
    var bar: Int
    init(foo: String, bar: Int) {
        self.bar = bar
        super.init(foo: foo)
    }
}

// ...

This time, we get

error: 'required' initializer 'init(from:)' must be provided by subclass of 'BaseClass'
}
^
<unknown>:0: note: 'required' initializer is declared in superclass here

This is the error message you get when you need to provide an implementation for a required initializer but have provided a different initializer, and new requirements on the class prevent inheritance. For instance:

class BaseClass {
    required init() {} // note: 'required' initializer is declared in superclass here
}

class SuperClass : BaseClass {
    var foo: Int
    init(foo: Int) { self.foo = foo }
} // error: 'required' initializer 'init()' must be provided by subclass of 'BaseClass'

If SuperClass didn't provide its own init(foo:) you'd get a different error message saying it has no initializers (your original error message above). In any case, we can get around this by providing foo with a default value instead of assigning in an init:

class BaseClass {
    required init() {}
}

class SuperClass : BaseClass {
    var foo: Int = 3
}

This allows inheritance of required init() while still initializing SuperClass.foo. Let's apply that to our situation as well:

class BaseClass : Decodable {
    var foo: String = "This is some default"
}

class SuperClass : BaseClass {
    var bar: Int = 3
}

// ...

This compiles just fine — but, does SuperClass here get its own init(from:), or is it inheriting BaseClass.init(from:)? If we try with the same JSON payload, we get the following error:

Swift.DecodingError.keyNotFound(CodingKeys(stringValue: "foo", intValue: nil), Swift.DecodingError.Context(codingPath: [], debugDescription: "No value associated with key CodingKeys(stringValue: \"foo\", intValue: nil) (\"foo\").", underlyingError: nil))

This error boils down to: "we tried to find "foo" as a key in the root dictionary (codingPath is empty), but failed to find it". Which is true, because the JSON payload we're using has foo nested:

{
    "bar": 42,
    "super": { "foo": "Hello, world!" }
}

For the sake of the test, let's un-nest to see what happens:

let json = """
{
    "bar": 42,
    "foo": "Hello, world!"
}
""".data(using: .utf8)!

let superClass = try! decoder.decode(SuperClass.self, from: json)
print(superClass.foo, superClass.bar) // Hello, world! 3

We're now getting bar's default value rather than the value in the JSON — that's because SuperClass has inherited BaseClass.init(from:) directly, and BaseClass.init(from:) has no knowledge of bar; SuperClass has to then use bar's default value.

If, by the way, we tried to remove the default value from bar, we'd see the following errors:

error: class 'SuperClass' has no initializers
class SuperClass : BaseClass {
      ^
note: did you mean to override 'init(from:)'?
class SuperClass : BaseClass {
      ^
note: stored property 'bar' without initial value prevents synthesized initializers
    var bar: Int
        ^
                 = 0

That first note is the relevant one: we can't inherit init(from:) so we would otherwise need to override it. (The "synthesized initializers" note refers only to synthesizing a default constructor of init(), not init(from:).


This case would be improved if the compiler could synthesize SuperClass.init(from:) instead of inheriting, but it won't be able to without a refactor of Swift's protocol conformance and inheritance system (and without syntax to disambiguate between "I'm not providing an implementation because I'd like to inherit" vs. "I'm not providing an implementation because I'd like to synthesize")

1 Like

This better explains the behavior I've observed. Given the behavior, I assumed that protocol conformance isn't inherited, but now that I think about it, the sub-class inherits the implementation that supports conformance and hence it must also conform. However, it doesn't, and you have explained this in a later post. Thank you.

Yep! Happy to have helped clarify. To thoroughly cover the other possible combinations too:

  • If a class re-declares conformance that the parent class already declares, you'll currently get errors:

    class Foo : Codable {}
    class Bar : Foo, Codable {}
    

    results in

    /private/tmp/Playground.swift:2:18: error: redundant conformance of 'Bar' to protocol 'Decodable'
    class Bar : Foo, Codable {}
                     ^
    /private/tmp/Playground.swift:2:7: note: 'Bar' inherits conformance to protocol 'Decodable' from superclass here
    class Bar : Foo, Codable {}
          ^
    /private/tmp/Playground.swift:2:18: error: redundant conformance of 'Bar' to protocol 'Encodable'
    class Bar : Foo, Codable {}
                     ^
    /private/tmp/Playground.swift:2:7: note: 'Bar' inherits conformance to protocol 'Encodable' from superclass here
    class Bar : Foo, Codable {}
          ^
    
  • If a class inherits from a non-Codable class, you'll synthesize conformance in the subclass:

    class Foo {}
    class Bar : Foo, Codable { /* ... */ } // <- gets synthesis
    

I've been playing with this to clarify my understanding. The last combination does result in synthesized conformation, but only for the sub-class. For example:

class A {
    var a1: Int
    
    init() {
        a1 = 10
    }
}

class B: A, Codable {
    var b1: Int
    
    override init() {
        b1 = 20
        super.init()
     }

}

let b = B()

let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted

let data = try encoder.encode(b)
print(String(data: data, encoding: .utf8))

This code prints to the console:

Optional("{\n  \"b1\" : 20\n}")

Yes, that's correct — see my second bullet point above. In this case, A itself isn't Codable, so there's no init(from:) to inherit. From a synthesis perspective, A may as well not exist, save for the properties which B inherits from it.

Slightly more formally: during type checking, the compiler encounters B, which claims to conform to Codable. When type checking this conformance, the compiler looks through all of the requirements that Codable (Encodable and Decodable) imposes; for each of those requirements, the type checker looks through B for a match.

Matches can be found by being inherited from the superclass, or by an explicit member on the type itself (i.e. if B implemented init(from:) itself). If no match is found, some protocols like Encodable and Decodable can get synthesized members which satisfy those requirements.

In the case of B here, because it doesn't inherit init(from:) from A to satisfy the requirement, and doesn't supply its own, it gets a synthesized implementation. (This is as opposed to a class like BaseClass, whose match is found by directly inheriting it from the superclass. This is why synthesis currently does not happen: the inherited match is necessarily preferred over synthesizing a new one.)

Similar question,

class Foo { //}: Codable {

}

class Bar : Foo {
func c() -> Int {
return 2;
}
}

let x = Bar()
let y = x.c() // <-throws bad_exc when codable is uncommented

What am I missing?

What version of Swift (and Xcode) are you using? Sounds like this could be SR-6468 which was fixed a little while back. I'm not seeing it on the latest beta.

Swift 4.1

works if i move the function to the base class

Sounds like the issue above, then, which is resolved for Swift 4.2

This line code gives me error
try super.init(from: container.superDecoder())
It should be
try super.init(from: decoder)

What is the error you're seeing? If you're using a KeyedDecodingContainer (i.e., you called decoder.container(keyedBy: ...) and are using that container), the .superDecoder() call will need to be .superDecoder(forKey: ...). [Unless you have specific format requirements and know what you're doing, it's relatively rare to need to pass your own decoder into super.init(from:)]