Define a type EmptyMessage like:
message EmptyMessage {
// nothing
}
Now parse your message as EmptyMessage, then call toString() on it.
Why does this work? Well, consider that it is backwards-compatible to add fields to a message type. When you add a field, then send a message using that field to an old program that wasn't build with knowledge of the field, then the field is treated as as "unknown field". Unknown fields are printed as number / value pairs. Now, if you start with EmptyMessage and add fields, you can obviously get any other message. Therefore, all message types are "backwards-compatible" with EmptyMessage. Therefore, any message can be parsed as EmptyMessage to treat all the fields as unknown fields.