<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://fpseverino.com/feed.xml" rel="self" type="application/atom+xml" /><link href="https://fpseverino.com/" rel="alternate" type="text/html" /><updated>2026-01-14T21:25:38+00:00</updated><id>https://fpseverino.com/feed.xml</id><title type="html">Francesco Paolo Severino</title><subtitle>Computer Engineering student 🇺🇦 🇵🇸</subtitle><author><name>Francesco Paolo Severino</name></author><entry><title type="html">The Dangers of Protocol Conformances in Swift</title><link href="https://fpseverino.com/swift/2025/12/16/protocol-conformances.html" rel="alternate" type="text/html" title="The Dangers of Protocol Conformances in Swift" /><published>2025-12-16T00:00:00+00:00</published><updated>2025-12-16T00:00:00+00:00</updated><id>https://fpseverino.com/swift/2025/12/16/protocol-conformances</id><content type="html" xml:base="https://fpseverino.com/swift/2025/12/16/protocol-conformances.html"><![CDATA[<p>A recent release of Swift Log (v1.8.0), which includes a PR I authored, has caused a ripple effect of unexpected breaking changes across the Swift Package ecosystem.
This post explores what happened, why it happened, and how we can avoid similar issues in the future.</p>

<h2 id="the-pull-request">The Pull Request</h2>

<p>In Swift server-side applications, logging is handled by <a href="https://github.com/apple/swift-log">Swift Log</a>, a logging API and implementation library maintained by Apple, widely used across the ecosystem.</p>

<p>As part of my contributions to Vapor 5, I moved some logging-related functionality from Vapor to ConsoleKit v5 and Swift Configuration.
One of the few things left in Vapor in that regard was a simple extension to make <code class="language-plaintext highlighter-rouge">Logger.Level</code> (a type from Swift Log) <code class="language-plaintext highlighter-rouge">@retroactive</code>ly conform to <code class="language-plaintext highlighter-rouge">CustomStringConvertible</code> and <code class="language-plaintext highlighter-rouge">LosslessStringConvertible</code>, two standard library protocols.</p>

<p><code class="language-plaintext highlighter-rouge">@retroactive</code> is a Swift attribute that warns you when you conform types you don’t own to protocols you don’t own.
It exists to make you aware of the risks you’re taking: if the type later adopts the same protocol, you end up in non-deterministic behavior, as the compiler won’t know which conformance to use.
You can read more about <code class="language-plaintext highlighter-rouge">@retroactive</code> in <a href="https://github.com/swiftlang/swift-evolution/blob/main/proposals/0364-retroactive-conformance-warning.md">SE-0364</a>.</p>

<p>Going back to the <code class="language-plaintext highlighter-rouge">@retroactive</code> conformances in Vapor, since these were conformances to standard library protocols that I knew were fairly widely replicated across the ecosystem and that would be generally useful, I decided to propose moving them directly into Swift Log instead.
So, I opened <a href="https://github.com/apple/swift-log/pull/395">apple/swift-log#395</a>, motivating the reasons behind this change.</p>

<blockquote>
  <p>Contents of the PR (minus tests):</p>
</blockquote>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">extension</span> <span class="kt">Logger</span><span class="o">.</span><span class="kt">Level</span><span class="p">:</span> <span class="kt">CustomStringConvertible</span><span class="p">,</span> <span class="kt">LosslessStringConvertible</span> <span class="p">{</span>
    <span class="c1">/// A textual representation of the log level.</span>
    <span class="kd">public</span> <span class="k">var</span> <span class="nv">description</span><span class="p">:</span> <span class="kt">String</span> <span class="p">{</span>
        <span class="k">self</span><span class="o">.</span><span class="n">rawValue</span>
    <span class="p">}</span>

    <span class="c1">/// Creates a log level from its textual representation.</span>
    <span class="c1">/// - Parameter description: A textual representation of the log level, case insensitive.</span>
    <span class="kd">public</span> <span class="nf">init</span><span class="p">?(</span><span class="n">_</span> <span class="nv">description</span><span class="p">:</span> <span class="kt">String</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">self</span><span class="o">.</span><span class="nf">init</span><span class="p">(</span><span class="nv">rawValue</span><span class="p">:</span> <span class="n">description</span><span class="o">.</span><span class="nf">lowercased</span><span class="p">())</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The PR was reviewed, accepted and merged, and Swift Log v1.8.0 was released shortly after.
It was rightly released as a minor version bump, as conformances to standard library protocols and new APIs, such as computed variables and initializers, are not considered breaking changes by SemVer 2.0.</p>

<h2 id="unexpected-consequences">Unexpected Consequences</h2>

<p>Shortly after the release, discussions started about how SwiftPM would handle the <code class="language-plaintext highlighter-rouge">@retroactive</code> conformances in libraries.</p>

<blockquote>
  <p>@fpseverino on Discord:</p>

  <p>It’s gonna look great on my resume: “Destroyed the whole Swift package ecosystem”</p>
</blockquote>

<p>Soon enough, conflicts started to appear: the first library to fall victim was Vapor itself.</p>

<h3 id="retroactive-conformances">@retroactive Conformances</h3>

<p>When trying to use <code class="language-plaintext highlighter-rouge">logLevel.description</code> or the <code class="language-plaintext highlighter-rouge">LosslessStringConvertible</code> initializer, users of Vapor would get a compiler error about ambiguous use of those members.</p>

<p><img src="/assets/protocol-conformances/vapor-error.png" alt="Vapor error screenshot" /></p>

<p>The issue was that both Vapor and Swift Log were providing conformances to those protocols for <code class="language-plaintext highlighter-rouge">Logger.Level</code>.
As previously mentioned, the conformances in Vapor were marked as <code class="language-plaintext highlighter-rouge">@retroactive</code>, which means that you, the author of said conformance, acknowledge that if the type being conformed to (which you don’t own) later provides the same conformance you end up in non-deterministic behavior.
And that is exactly what happened here: Swift Log added the same conformances, and now the compiler couldn’t decide which one to use.
The issue was quickly fixed by removing the conformances from Vapor, and a new release (<a href="https://github.com/vapor/vapor/releases/tag/4.120.0">v4.120.0</a>) was cut.</p>

<h3 id="combining-multiple-protocol-conformances">Combining Multiple Protocol Conformances</h3>

<p>Next, a different issue appeared in the Hummingbird template.
The template uses Swift Argument Parser to get the log level from command line arguments, so it had an empty extension that <code class="language-plaintext highlighter-rouge">@retroactive</code>ly made <code class="language-plaintext highlighter-rouge">Logger.Level</code> conform to <code class="language-plaintext highlighter-rouge">ExpressibleByArgument</code>.</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">extension</span> <span class="kt">Logger</span><span class="o">.</span><span class="kt">Level</span><span class="p">:</span> <span class="kd">@retroactive</span> <span class="kt">ExpressibleByArgument</span> <span class="p">{}</span>
</code></pre></div></div>

<p>That worked because Swift Argument Parser has two extensions to <code class="language-plaintext highlighter-rouge">RawRepresentable</code> and <code class="language-plaintext highlighter-rouge">LosslessStringConvertible</code> that provide a default initializer for <code class="language-plaintext highlighter-rouge">ExpressibleByArgument</code> for types conforming to one of those protocols.</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">extension</span> <span class="kt">RawRepresentable</span> <span class="k">where</span> <span class="k">Self</span><span class="p">:</span> <span class="kt">ExpressibleByArgument</span><span class="p">,</span> <span class="kt">RawValue</span><span class="p">:</span> <span class="kt">ExpressibleByArgument</span> <span class="p">{</span>
    <span class="kd">public</span> <span class="nf">init</span><span class="p">?(</span><span class="nv">argument</span><span class="p">:</span> <span class="kt">String</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">if</span> <span class="k">let</span> <span class="nv">value</span> <span class="o">=</span> <span class="kt">RawValue</span><span class="p">(</span><span class="nv">argument</span><span class="p">:</span> <span class="n">argument</span><span class="p">)</span> <span class="p">{</span>
            <span class="k">self</span><span class="o">.</span><span class="nf">init</span><span class="p">(</span><span class="nv">rawValue</span><span class="p">:</span> <span class="n">value</span><span class="p">)</span>
        <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
            <span class="k">return</span> <span class="kc">nil</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="kd">extension</span> <span class="kt">LosslessStringConvertible</span> <span class="k">where</span> <span class="k">Self</span><span class="p">:</span> <span class="kt">ExpressibleByArgument</span> <span class="p">{</span>
    <span class="kd">public</span> <span class="nf">init</span><span class="p">?(</span><span class="nv">argument</span><span class="p">:</span> <span class="kt">String</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">self</span><span class="o">.</span><span class="nf">init</span><span class="p">(</span><span class="n">argument</span><span class="p">)</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Before Swift Log v1.8.0, <code class="language-plaintext highlighter-rouge">Logger.Level</code> only conformed to <code class="language-plaintext highlighter-rouge">RawRepresentable</code>, so there was no ambiguity.
After the release, however, <code class="language-plaintext highlighter-rouge">Logger.Level</code> conformed to both <code class="language-plaintext highlighter-rouge">RawRepresentable</code> <strong>and</strong> <code class="language-plaintext highlighter-rouge">LosslessStringConvertible</code>, so the compiler couldn’t decide which initializer to use.
So the issue really lied in Swift Argument Parser, which didn’t account for the possibility of a type conforming to both protocols.
The Hummingbird template <em>has</em> to use a <code class="language-plaintext highlighter-rouge">@retroactive</code> conformance in this case, as Swift Argument Parser and Swift Log are (presumably) never going to depend on each other.
The Hummingbird template was quickly fixed too, by providing a custom implementation to <code class="language-plaintext highlighter-rouge">ExpressibleByArgument</code> instead.
The ambiguity issue was fixed in Swift Argument Parser as well, in <a href="https://github.com/apple/swift-argument-parser/pull/841">apple/swift-argument-parser#841</a>, by adding a more specific extension for types conforming to both protocols.</p>

<h3 id="capturing-function-signatures-without-spelling-out-the-parameter-labels">Capturing Function Signatures without Spelling Out the Parameter Labels</h3>

<p>The last library (so far) to be affected was the Swift AWS Lambda Runtime, and the issue was completely different from the previous two.
The library didn’t provide any <code class="language-plaintext highlighter-rouge">@retroactive</code> conformance, neither did it expect <code class="language-plaintext highlighter-rouge">Logger.Level</code> to conform to any protocol, like in the Hummingbird/ArgumentParser case.
Instead, the issue was that Swift AWS Lambda Runtime used the <code class="language-plaintext highlighter-rouge">Logger.Level</code> initializer provided by <code class="language-plaintext highlighter-rouge">RawRepresentable</code> inside a mapping function, passing directly the initializer signature to <code class="language-plaintext highlighter-rouge">flatMap</code>, without spelling out the parameter label, like so:</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">log</span><span class="o">.</span><span class="n">logLevel</span> <span class="o">=</span> <span class="kt">Lambda</span><span class="o">.</span><span class="nf">env</span><span class="p">(</span><span class="s">"LOG_LEVEL"</span><span class="p">)</span><span class="o">.</span><span class="nf">flatMap</span><span class="p">(</span><span class="kt">Logger</span><span class="o">.</span><span class="kt">Level</span><span class="o">.</span><span class="kd">init</span><span class="p">)</span> <span class="p">??</span> <span class="n">logger</span><span class="o">.</span><span class="n">logLevel</span>
</code></pre></div></div>

<p>The compiler was able to resolve the call to <code class="language-plaintext highlighter-rouge">Logger.Level.init</code> before Swift Log v1.8.0, because there was only one initializer available that took a <code class="language-plaintext highlighter-rouge">String</code>: the one provided by <code class="language-plaintext highlighter-rouge">RawRepresentable</code>.
After the release, however, there was now a second initializer available, the one provided by <code class="language-plaintext highlighter-rouge">LosslessStringConvertible</code>, that also takes a <code class="language-plaintext highlighter-rouge">String</code>, so the compiler couldn’t decide which one to use.
The issue has been fixed in <a href="https://github.com/awslabs/swift-aws-lambda-runtime/pull/619">awslabs/swift-aws-lambda-runtime#619</a> by calling <code class="language-plaintext highlighter-rouge">flatMap</code> with a closure that explicitly calls the desired initializer, like so:</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">log</span><span class="o">.</span><span class="n">logLevel</span> <span class="o">=</span> <span class="kt">Lambda</span><span class="o">.</span><span class="nf">env</span><span class="p">(</span><span class="s">"LOG_LEVEL"</span><span class="p">)</span><span class="o">.</span><span class="n">flatMap</span> <span class="p">{</span> <span class="o">.</span><span class="nf">init</span><span class="p">(</span><span class="nv">rawValue</span><span class="p">:</span> <span class="nv">$0</span><span class="p">)</span> <span class="p">}</span> <span class="p">??</span> <span class="n">logger</span><span class="o">.</span><span class="n">logLevel</span>
</code></pre></div></div>

<p>Another possible fix would have been to spell out the parameter label when passing the initializer to <code class="language-plaintext highlighter-rouge">flatMap</code>, like so:</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">log</span><span class="o">.</span><span class="n">logLevel</span> <span class="o">=</span> <span class="kt">Lambda</span><span class="o">.</span><span class="nf">env</span><span class="p">(</span><span class="s">"LOG_LEVEL"</span><span class="p">)</span><span class="o">.</span><span class="nf">flatMap</span><span class="p">(</span><span class="kt">Logger</span><span class="o">.</span><span class="kt">Level</span><span class="o">.</span><span class="nf">init</span><span class="p">(</span><span class="nv">rawValue</span><span class="p">:))</span> <span class="p">??</span> <span class="n">logger</span><span class="o">.</span><span class="n">logLevel</span>
</code></pre></div></div>

<h2 id="what-we-learned">What We Learned</h2>

<p>There are a few lessons that I think we can learn from this incident:</p>

<ol>
  <li>
    <p>We shouldn’t extend types we don’t own with <code class="language-plaintext highlighter-rouge">@retroactive</code> conformances to protocols we don’t own, especially <code class="language-plaintext highlighter-rouge">public</code>ly.
 This applies in particular to standard library protocols, which are more likely to be adopted by types in the future.
 <code class="language-plaintext highlighter-rouge">@retroactive</code> and its warning exist exactly for this reason, to make you aware of the risks you’re taking.</p>
  </li>
  <li>
    <p>While very similar, <code class="language-plaintext highlighter-rouge">RawRepresentable</code>, <code class="language-plaintext highlighter-rouge">CustomStringConvertible</code> and <code class="language-plaintext highlighter-rouge">LosslessStringConvertible</code> serve different purposes, and it’s not unlikely that a type would want to conform to more than one of them.
 Libraries that provide default implementations based on those protocols (like Swift Argument Parser) should take this into account, and avoid ambiguities.</p>
  </li>
  <li>
    <p>We should put extra thought when working with remote library packages, both as users and as contributors/maintainers.
 As users, we should be aware that we don’t own the code, and even if it follows SemVer 2.0 it might change in unexpected ways, as we’ve seen.
 As contributors and maintainers, we should be aware that even additive changes, especially to widely used libraries like Swift Log, can have far-reaching and unpredictable consequences (see also <a href="https://www.hyrumslaw.com">Hyrum’s Law</a>).</p>
  </li>
  <li>
    <p>And last but not least, the Swift open source community once again proved to be amazing, quickly identifying and fixing the issues caused by this change.
 Thanks to everyone involved in the discussions and fixes!</p>
  </li>
</ol>]]></content><author><name>Francesco Paolo Severino</name></author><category term="swift" /><summary type="html"><![CDATA[A recent release of Swift Log (v1.8.0), which includes a PR I authored, has caused a ripple effect of unexpected breaking changes across the Swift Package ecosystem. This post explores what happened, why it happened, and how we can avoid similar issues in the future.]]></summary></entry></feed>