Merging one-to-many and one-to-one relation in the same JSON object in postgresql
See the composite types documentation for how to handle entire rows as a single value (which ends up as an object in JSON).

Apparently, the only way to get the correct field names for the extra info is to create a new type for that subobject:

CREATE TYPE extra_info_object AS (
  extra_info1 TEXT,
  extra_info2 TEXT
);

SELECT to_json(data)
FROM (SELECT name,
             ROW(extra_info1,
                 extra_info2)::extra_info_object AS extra_info,
             (SELECT array_agg(events)
              FROM events
              WHERE events.op_id = ops.op_id
             ) AS events
      FROM ops
      LEFT JOIN op_extra_info USING (op_id)
) AS data;