Search code examples
jsonpostgresqlcomposite-types

Converting json to nested postgres composite type


I have the following nested types defined in postgres:

CREATE TYPE address AS (
  name    text,
  street  text,
  zip     text,
  city    text,
  country text
);

CREATE TYPE customer AS (
  customer_number           text,
  created                   timestamp WITH TIME ZONE,
  default_billing_address   address,
  default_shipping_address  address
);

And would now like to populate this types in a stored procedure, which gets json as an input parameter. This works for fields on the top-level, the output shows me the internal format of a postgres composite type:

# select json_populate_record(null::customer, '{"customer_number":"12345678"}'::json)::customer;
 json_populate_record 
----------------------
 (12345678,,,)
(1 row)

However, postgres does not handle a nested json structure:

# select json_populate_record(null::customer, '{"customer_number":"12345678","default_shipping_address":{"name":"","street":"","zip":"12345","city":"Berlin","country":"DE"}}'::json)::customer;
ERROR:  malformed record literal: "{"name":"","street":"","zip":"12345","city":"Berlin","country":"DE"}"
DETAIL:  Missing left parenthesis.

What works again is, if the nested property is in postgres' internal format like here:

# select json_populate_record(null::customer, '{"customer_number":"12345678","default_shipping_address":"(\"\",\"\",12345,Berlin,DE)"}'::json)::customer;
            json_populate_record            
--------------------------------------------
 (12345678,,,"("""","""",12345,Berlin,DE)")
(1 row)

Is there any way to get postgres to convert from a nested json structure to a corresponding composite type?


Solution

  • plpython to the rescue:

    create function to_customer (object json)
    returns customer
    AS $$
    import json
    return json.loads(object)
    $$ language plpythonu;
    

    Example:

    select to_customer('{
            "customer_number":"12345678",
            "default_shipping_address":
            {
                    "name":"",
                    "street":"",
                    "zip":"12345",
                    "city":"Berlin",
                    "country":"DE"
            },
            "default_billing_address":null,
            "created": null
    }'::json);
                    to_customer                 
    --------------------------------------------
     (12345678,,,"("""","""",12345,Berlin,DE)")
    (1 row)
    

    Warning: postgresql when building returned object from python requires to have all null values present as None (ie. it's not allowed to skip null values as not present), thus we have to specify all null values in incoming json. For example, not allowed:

    select to_customer('{
            "customer_number":"12345678",
            "default_shipping_address":
            {
                    "name":"",
                    "street":"",
                    "zip":"12345",
                    "city":"Berlin",
                    "country":"DE"
            } 
    }'::json);                             
    ERROR:  key "created" not found in mapping
    HINT:  To return null in a column, add the value None to the mapping with the key named after the column.
    CONTEXT:  while creating return value
    PL/Python function "to_customer"