Imagine you have two servers communicating with
enum Packet { Ping, Pong, }
Where the semantics are that if you receive a ping, you say pong, and if you see pong, you say ping.
Suppose we deploy this out to several servers, with code built on 2020-7-12. Now on 2020-07-13 you commit to head a new enum value `Stats`, which returns a new kind of reply.
If you send outgoing requests in the same 2020-07-13 build, you're going to have a bad time, as your fleet is going to start at 100% 2020-07-12 and switch over to 100% 2020-07-13, but during that switch you'll have some 50% 50% split. If a new server sends a request to an old one, you'll have a crash.
Instead you have to add receivers in 2020-07-13, but not use it yet. Roll it out, make sure it's past how far you might roll back, then you can deploy a 2020-07-20 build which makes calls using the new enum.
If you skip that step, you're going to have a bad time. Maybe you want to instead have a default case handler instead of crashing, but that means each of your enums has to know about "UNKNOWN" and you're deserialization error has to be future proof.
I wrote the Haskell implementation. It maps unions to variants (what Rust calls enums), BUT it always generates an extra 'unkown' variant, which gets used whenever the variant is one not recognized by the generated code.
In this case the value proposition of variants/enums, including exhaustiveness checking, is still really useful -- it can not only deal with the possibility of more variants added in the future, it forces you to handle that possibility, which imo is the best of both worlds.
But yes, in general, when modeling data that comes from the outside world, you need to about overfitting.
Vaguely related: https://lexi-lambda.github.io/blog/2020/01/19/no-dynamic-typ...