Skip to content

PolymorphicManyToOne serializes an empty id in list view → frontend crashes with "Expected id to be a string or number, received null" #332

Description

@benoitbargesCP

Environment

  • forest_admin_agent 1.35.3
  • forest_admin_datasource_active_record 1.35.3
  • forest_admin_rails 1.35.3
  • Ruby 4.0.2 / Rails 8.1, PostgreSQL
  • support_polymorphic_relations: true enabled on the datasource

Summary

When a collection with a polymorphic belongs_to contains at least one record, opening its list view fails on the frontend with:

UnknownClientRecordAgentError: Expected id to be a string or number, received null
  at O.fetchRecords (feature.ts:440)
{requestType: 'list'}

The list becomes completely unusable ("An error occurred. Please try again…"). The backend returns 200 OK — the payload itself is malformed.

Reproduction

Models:

class Address < ApplicationRecord
  belongs_to :addressable, polymorphic: true
end

class Contact < ApplicationRecord
  has_one :address, as: :addressable
end

Create one Address linked to a Contact, then open the Address list in the UI.

Root cause

For a PolymorphicManyToOne, QueryStringParser#build_projection_fields builds a wildcard sub-projection:

# forest_admin_agent/utils/query_string_parser.rb:72
when 'PolymorphicManyToOne'
  "#{field_name}:*"

But a polymorphic belongs_to can't be SQL-joined, so the AR datasource skips it while building the SELECT:

# forest_admin_datasource_active_record/utils/query.rb:208
next if relation_schema.type == 'PolymorphicManyToOne'

The record therefore comes back with a phantom related object that has no primary key:

{ "addressable_id" => 1, "addressable_type" => "Contact", "addressable" => { "*" => nil } }

The serializer then reads the pk from that phantom record, producing an empty id in two places:

  1. Relationship linkage:
"addressable": { "data": { "type": "Contact", "id": "" } }
  1. Compound included document (note the empty self-link):
"included": [{ "type": "Contact", "id": "", "links": { "self": "/forest/Contact/" } }]

EmberData rejects the empty id → the whole list fails to render.

Expected behavior

The linkage id (and the included resource id, if any) should be built from the foreign key already present on the owner record (addressable_id + addressable_type), which does not require loading the related record.

Notes

  • Only reproduces when the collection has ≥ 1 record (empty polymorphic collections render fine, which masks the bug).

Workaround (in our app, not a gem fix)

Since the related record's primary key can't come from a SQL join for a polymorphic
belongs_to, we rebuild it from the foreign key already present on the owner record.
No extra query is issued (the FK is already loaded in memory):

ForestAdminAgent::Serializer::ForestSerializer.prepend(Module.new do
  def has_one_relationship(attribute_name, attr_data)
    object = super
    return object unless object.is_a?(Hash)

    collection = ForestAdminAgent::Facades::Container.datasource.get_collection(
      @options[:class_name].gsub("::", "__")
    )
    field = collection.schema[:fields][attribute_name.to_s]
    return object unless field&.type == "PolymorphicManyToOne"

    foreign_key = @object[field.foreign_key]
    foreign_type = @object[field.foreign_key_type_field]
    return nil if foreign_key.nil? || foreign_type.nil?

    primary_key = (field.foreign_key_targets || {})[foreign_type] || "id"
    object.merge(primary_key => foreign_key)
  end
end)

Verified: fixes both the relationship linkage and the included document, no N+1
(0 extra SQL queries measured serializing 50 polymorphic rows), and unlinked
records (foreign_key nil) correctly omit the data key instead of crashing.

The proper fix probably belongs in ForestSerializer#has_one_relationship or in the
datasource's projection handling for PolymorphicManyToOne (query.rb:208), so the
sub-projection isn't silently skipped.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions