Composite Primary Keys

View Source

When working with resources that have composite primary keys (multiple fields that together form the unique identifier), AshJsonApi provides special support for encoding and decoding these keys in URLs.

Defining Composite Primary Keys

First, define your composite primary key in the JSON API configuration:

defmodule MyApp.Bio do
  use Ash.Resource,
    extensions: [AshJsonApi.Resource]

  attributes do
    attribute :author_id, :uuid, primary_key?: true, public?: true
    attribute :category, :string, primary_key?: true, public?: true
    attribute :content, :string, public?: true
  end

  json_api do
    type "bio"
    
    primary_key do
      keys [:author_id, :category]
      delimiter "|"  # Use a delimiter that won't conflict with your data
    end
  end
end

Important Considerations for Delimiters

When choosing a delimiter, ensure it won't appear in your actual data:

  • UUIDs contain dashes (-) - Don't use - as a delimiter if any of your composite key fields are UUIDs
  • Safe alternatives: |, ~, ::, or other characters unlikely to appear in your data
  • Default delimiter: If not specified, AshJsonApi uses - as the default delimiter

Enabling Composite Key Parsing in Routes

To enable automatic parsing of composite primary keys in URL paths, you must opt-in by specifying the path_param_is_composite_key option on your routes:

json_api do
  type "bio"
  
  primary_key do
    keys [:author_id, :category]
    delimiter "|"
  end
  
  routes do
    base "/bios"
    
    # Enable composite key parsing for the :id parameter
    get :read, path_param_is_composite_key: :id
    patch :update, path_param_is_composite_key: :id
    delete :destroy, path_param_is_composite_key: :id
    
    # Other routes that don't need composite key parsing
    index :read
    post :create
  end
end

How It Works

With the above configuration:

  1. URL Format: /bios/550e8400-e29b-41d4-a716-446655440000|sports
  2. Parsing: The :id parameter 550e8400-e29b-41d4-a716-446655440000|sports gets split by the | delimiter
  3. Mapping: The parts are mapped to the primary key fields in order:
    • author_id = 550e8400-e29b-41d4-a716-446655440000
    • category = sports
  4. Filtering: The query is filtered to find the record with both author_id and category matching

Example Usage

# Creating a bio
POST /bios
{
  "data": {
    "type": "bio",
    "attributes": {
      "author_id": "550e8400-e29b-41d4-a716-446655440000",
      "category": "sports",
      "content": "Author bio for sports category"
    }
  }
}

# Retrieving the bio using composite key
GET /bios/550e8400-e29b-41d4-a716-446655440000|sports

# Updating the bio
PATCH /bios/550e8400-e29b-41d4-a716-446655440000|sports
{
  "data": {
    "type": "bio", 
    "attributes": {
      "content": "Updated bio content"
    }
  }
}

# Deleting the bio
DELETE /bios/550e8400-e29b-41d4-a716-446655440000|sports

Without Opt-In Parsing

If you don't specify path_param_is_composite_key on a route, the path parameter will be treated as a regular single value, even if your resource has composite primary keys defined. This ensures backward compatibility and prevents unexpected behavior.

Error Handling

If the composite key format is invalid (wrong number of parts after splitting), AshJsonApi will return a 404 Not Found error with appropriate JSON:API error formatting.