You can always cast to text yourself, of course, but I am not familiar with the type hierarchy enough to tell why `to_json` can't deduce that as text whereas the other function can.
My understanding is that "any" is defined to accept that behavior - allowing any pseudo-type and unknown. The "anyelement" polymorphic pseudo-type is defined such that only concrete known types are allowed to match - and then the rules of polymorphism apply when performing a lookup. My uninformed conclusion is that since to_json only defines a single parameter that changing it from "anyelement" to "any" would be reasonable and the hack describe probably "just works" (though I'd test it on a wide-range of built-in types first if I was actually going to use the hack).
You only get to use "any" for a C-language function but that is indeed the case here.
Type "anyelement" can force the function's result type directly. But there cannot be function that returns UNKNOWN.
Type "any" just accept any argument without any impact on result type. Unfortunately, inside a function is necessary to do much more work related to casting types, and the execution can be slower.
I checked the source code of to_json and this function can use "any" without any change.