"Row polymorphism" sounds like exactly what protobuf does, which I think is the right answer.
As I mentioned, I've heard people argue that the client and server should pre-negotiate exactly which version of the protocol they are using, and then that version should have a rigidly-defined schema. It may have been unfair to suggest that this is a common opinion among type theorists -- I don't have that many data points.
Row polymorphism is a more principled approach that makes the type theorists happy.
The point of row polymorphism is not just that you can say "I may or may not have other fields, but I definitely have a 'name' and 'email' field", but that the extra unknown fields have a name that can be used to constrain other types.
The first type can act as a forward compatible passthrough because it says that whatever extra fields are in the input are also in the output. The second type promises that its output only has name and email fields.
The same applies to variant types: you can say that a variant has option A, option B, and other options rest:
(A | B | &rest) -> (A | B | &rest)
vs
(A | B | &rest) -> (A | B)
Functions of these types are polymorphic in the schema. For instance, the type (A | B | &rest) -> (A | B) can be instantiated with rest = (C | D) to get (A | B | C | D) -> (A | B).
So row types are fundamentally different in that it's the functions that that explicitly deal with multiple possible schemas. At the end of the day all of the rest variables get instantiated with explicit types to get a concrete instantiation of a function in which all the rest variables have been replaced by concrete types.
A serialisation library could use this to do version negotiation automatically. After it has negotiated a version, the library instantiates the functions so that the rest type variables get instantiated with the actual concrete version of the schema.
Different languages implement row types in different ways. Some compile row types in a way akin to Java's type erasure. They compile a single version of each polymorphic function that can be used regardless of how the rest parameters get instantiated. Some compile row types in a way akin to C#'s generics or C++ templates: they compile a separate version for each instantiation.
The advantage of the latter is that the data representation can be optimised with full knowledge of the concrete schema. If we have a function of type {name: string, email: string, &rest} -> {name: string, email: string, &rest} instantiated with rest = {age: int} then that compiles to a version of type {name: string, email: string, age: int} -> {name: string, email: string, age: int}. This compiles to faster code because the compiler statically knows the size of the thing.
In a client-server situation you wouldn't know the schema of rest until run time, so you'd need either have a JIT compiler that can compile new versions at run time, or specify a fixed number of options for rest at compile time. To update a client-server application you'd need to recompile both the client and server with support for the new version. That's not nice but it does not have a chicken and egg problem because both the client and server still support the old version too.
TL;DR: with row types schemas are always rigidly defined, it's the functions that can handle multiple schemas.
As I mentioned, I've heard people argue that the client and server should pre-negotiate exactly which version of the protocol they are using, and then that version should have a rigidly-defined schema. It may have been unfair to suggest that this is a common opinion among type theorists -- I don't have that many data points.