What is the current common practice for self-referential type?
It is "don't". You rearrange your data structure so that it isn't self-referential.
If you really-truly-positively need a self-referential type, you can try some of the 3rd-party crates, but I'm intentionally not going to recommend any of them, as even the most popular one contains soundness bugs.
The fallancy in this reasoning is that when you move something you have complete ownership over it, which means you can also get mutable access to it, which is in contradiction with the fact that you already have an immutable reference.
This is even more clear in this example since there's nothing preventing you from doing foo.a = Box::new(t); and making the b reference dangling.
error[E0502]: cannot borrow `foo` as mutable because it is also borrowed as immutable
15 | foo.b = foo.a.as_ref();
| -------------- immutable borrow occurs here
16 | use_foo(&mut foo);
| mutable borrow occurs here
| immutable borrow later used here
In general, you can construct this kind of self-referential structure, but you will be unable to do anything with it that you couldn't have done with local variables a and b not contained in a struct.
In that case — you only ever use &Foos — it would work, yes. But I would still recommend avoiding the struct and using separate variables, unless you have some specific use for needing to pass the &Foo around a lot.
You can also consider defining a struct that is all borrowed — contains only references and not the Box. There is no problem with passing that around or even mutating it.
A common pattern for this use-case, is to have the struct borrow from the source of truth data, rather than own the source of truth data and have references back into it. So your code looks something like:
The compiler then knows that the return value of decode_file borrows from the supplied FileData, and will insist that the owned data is kept alive for as long as the data that borrows from it is alive. Once you've extracted the interesting data, you can turn it into owned data, and drop the borrowed form and the source file if you so desire.
There's also the rabbit hole of ways to handle Zone versus BorrowedZone - you might make BorrowedZone a Cow<'a, (String, Zone)>, for example, or put the zone name into Zone, or otherwise make changes.