If you use row_to_json() , you will lose the column names unless you enter a string to be typed. If you manually create a jsonb object using json_build_object() using explicit names, you return them:
SELECT zoo.id, array_agg(za.jb) AS animals FROM zoo JOIN ( SELECT DISTINCT ON (zooId, "type", "name") zooId, json_build_object('animal_type', "type", 'animal_name', "name")::jsonb AS jb FROM animals ORDER BY zooId, jb->>'animal_type', jb->>'animal_name'
You can ORDER BY elements of a jsonb object as shown above, but (as far as I know) you cannot use DISTINCT for a jsonb object. In your case, this would be pretty inefficient in any case (building all the jsonb objects jsonb and then throwing out duplicates), and at the aggregate level, this is simply not possible with standard SQL. However, you can achieve the same result by applying the DISTINCT before creating the jsonb object.
Also, avoid using SQL keywords such as "type" and standard data types such as "name" as column names. Both are unreserved keywords, so you can use them in your contexts, but in practice your teams can become very confusing. For example, you might have a schema with a table, a column in that table and a data type, each of which is called a "type", and then you can get this:
SELECT type::type FROM type.type WHERE type = something;
While PostgreSQL will kindly agree with this, it is at best confused and error prone in all the more complex situations. You can get a long way by double quoting any keywords, but they are best avoided as identifiers.