Typically this gets implemented as a long chain of functions.
Every structure you write would begin with a version number, the object's ID, and typically the size of the block that was written. Then when you decode it, it often works something like this:
bool ConvertToV13( int version, const unsigned char* data )
{
if(version == 12 || ConvertToV12( version, data )) {
// Convert from 12 to 13.
}
}
}
bool ConvertToV12( int version, const unsigned char* data )
{
if(version == 11 || ConvertToV11( version, data ) {
// Convert from 11 to 12
}
}
}
bool ConvertToV11( int version, const unsigned char* data )
{
if(version == 10 || ConvertToV10( version, data ) {
// Convert from 10 to 11
}
}
}
...
It doesn't need to be a raw pointer to the data, there are a wide range of systems. Key/value pairs are fairly common, as are objects with bit packing and compression functionality. Even so, the code needs to handle migrating data from the earliest version of the structure up to the current form.
In some games I've worked on, there are also failsafe objects that are part of the root data structure. For example, on The Sims, if an object no longer exists in the database (perhaps they uninstalled a downloadable object) then it gets converted into the failsafe object instead that is part of the base game and guaranteed to exist. For example, a 1x2 DLC shop door object might have a failsafe of a basic door. A 2x2 DLC blackjack table object might have a failsafe object of a basic 2x2 wooden table. Since the failsafe object is part of the common root data structure, the failsafe could be used even if the object or the decoder for the object aren't available.
Serialization is mostly a solved problem, but just because it is solved doesn't mean there isn't work to do. Even when you use a great serialization library you still need to migrate between data versions.