The fine manual says that to_json's signature is:
to_json(anyelement)
so you're supposed say to_json(one_single_value). When you say:
to_json(users.email, users.phone_number)
you're trying to call to_json with two values and there is no such to_json function. When you say:
to_json(user.*)
you're actually calling to_json with a single ROW argument so it works just fine.
You can use a derived table or CTE as klin suggests or you can build the ROWs by hand:
select to_json(row(users.email, users.phone_number)) ...
The problem with this is that that ROW won't have any column names so your JSON will use useless keys like "f1" and "f2". To get around that you need to cast the ROW to something that does have names. One way to get some names is to create a custom type:
create type email_and_phone_number as (
email text,
phone_number text
)
and then cast your ROW to that type:
select to_json(row(users.email, users.phone_number)::email_and_phone_number) ...
You could also use a temporary table instead of a custom type:
create temporary table email_and_phone_number (
email text,
phone_number text
)
and then use the same cast as with a custom type.
If you're building this specific JSON format a lot then a custom type would make sense. If this is a one-off then a temporary table would make sense, the temporary table will automatically disappear when the session ends so there's nothing to clean up. Of course, a derived table or CTE might also make sense depending on the query and what tools you're using to interface with your database.