From 3aaf654263379bd926fecb233b2d5d221ec01ccc Mon Sep 17 00:00:00 2001 From: Barnabas Jovanovics Date: Mon, 22 Jun 2026 10:09:00 +0200 Subject: [PATCH] fix: Apply field_names mapping to OpenAPI filter schema The field_names DSL was respected at runtime for filter parsing (via filter_ref_transformer) but the generated OpenAPI spec still advertised filter properties and component schema ids under the raw internal field name. A resource with field_names(title: :subject) accepted filter[subject]=... at runtime but the spec advertised filter[title]=... Route the filter property keys (attribute/calculation/aggregate) and the "-filter-" component schema id through AshJsonApi.Resource.Info.field_to_json_key/2 so the spec matches runtime behavior. Relationship keys and boolean combinators are unaffected as they are not subject to field_names mapping. This also changes the filter property key type from atom to string, consistent with how the rest of the spec represents JSON:API keys. Co-Authored-By: Claude Opus 4.8 (1M context) --- lib/ash_json_api/json_schema/open_api.ex | 9 ++++---- test/acceptance/field_names_test.exs | 29 ++++++++++++++++++++++++ test/acceptance/open_api_test.exs | 28 +++++++++++------------ 3 files changed, 48 insertions(+), 18 deletions(-) diff --git a/lib/ash_json_api/json_schema/open_api.ex b/lib/ash_json_api/json_schema/open_api.ex index 38b3007..36d41fc 100644 --- a/lib/ash_json_api/json_schema/open_api.ex +++ b/lib/ash_json_api/json_schema/open_api.ex @@ -3039,7 +3039,8 @@ if Code.ensure_loaded?(OpenApiSpex) do end defp attribute_filter_field_type(resource, attribute) do - AshJsonApi.Resource.Info.type(resource) <> "-filter-" <> to_string(attribute.name) + AshJsonApi.Resource.Info.type(resource) <> + "-filter-" <> AshJsonApi.Resource.Info.field_to_json_key(resource, attribute.name) end defp resource_filter_fields(resource, domains) do @@ -3093,7 +3094,7 @@ if Code.ensure_loaded?(OpenApiSpex) do |> filter_shown_fields(resource) |> Enum.filter(&filterable?(&1, resource)) |> Enum.map(fn calculation -> - {calculation.name, + {AshJsonApi.Resource.Info.field_to_json_key(resource, calculation.name), %Reference{ "$ref": "#/components/schemas/#{attribute_filter_field_type(resource, calculation)}" }} @@ -3109,7 +3110,7 @@ if Code.ensure_loaded?(OpenApiSpex) do |> filter_shown_fields(resource) |> Enum.filter(&filterable?(&1, resource)) |> Enum.map(fn attribute -> - {attribute.name, + {AshJsonApi.Resource.Info.field_to_json_key(resource, attribute.name), %Reference{ "$ref": "#/components/schemas/#{attribute_filter_field_type(resource, attribute)}" }} @@ -3123,7 +3124,7 @@ if Code.ensure_loaded?(OpenApiSpex) do |> filter_shown_fields(resource) |> Enum.filter(&filterable?(&1, resource)) |> Enum.map(fn aggregate -> - {aggregate.name, + {AshJsonApi.Resource.Info.field_to_json_key(resource, aggregate.name), %Reference{ "$ref": "#/components/schemas/#{attribute_filter_field_type(resource, aggregate)}" }} diff --git a/test/acceptance/field_names_test.exs b/test/acceptance/field_names_test.exs index 73b00d1..ee56711 100644 --- a/test/acceptance/field_names_test.exs +++ b/test/acceptance/field_names_test.exs @@ -737,6 +737,35 @@ defmodule Test.Acceptance.FieldNamesTest do end end + # ─── OpenAPI filter schema ──────────────────────────────────────────────────── + + describe "field_names – OpenAPI filter schema" do + test "filter properties use the renamed attribute keys" do + api_spec = + AshJsonApi.Controllers.OpenApi.spec(%{private: %{}}, domains: [Domain]) + + filter_schema = api_spec.components.schemas["post-filter"] + + assert Map.has_key?(filter_schema.properties, "subject") + assert Map.has_key?(filter_schema.properties, "content") + refute Map.has_key?(filter_schema.properties, "title") + refute Map.has_key?(filter_schema.properties, "body") + end + + test "filter property $refs and component schema ids use the renamed keys" do + api_spec = + AshJsonApi.Controllers.OpenApi.spec(%{private: %{}}, domains: [Domain]) + + filter_schema = api_spec.components.schemas["post-filter"] + + assert filter_schema.properties["subject"]."$ref" == + "#/components/schemas/post-filter-subject" + + assert Map.has_key?(api_spec.components.schemas, "post-filter-subject") + refute Map.has_key?(api_spec.components.schemas, "post-filter-title") + end + end + # ─── Sparse fieldsets ───────────────────────────────────────────────────────── describe "field_names – sparse fieldsets (fields[])" do diff --git a/test/acceptance/open_api_test.exs b/test/acceptance/open_api_test.exs index 210af21..50bfb2e 100644 --- a/test/acceptance/open_api_test.exs +++ b/test/acceptance/open_api_test.exs @@ -467,10 +467,10 @@ defmodule Test.Acceptance.OpenApiTest do refute Map.has_key?(relationships, :hidden_author) filter_schema = api_spec.components.schemas["hidden-spec-post-filter"] - assert Map.has_key?(filter_schema.properties, :name) + assert Map.has_key?(filter_schema.properties, "name") assert Map.has_key?(filter_schema.properties, :visible_author) - refute Map.has_key?(filter_schema.properties, :secret) - refute Map.has_key?(filter_schema.properties, :secret_calc) + refute Map.has_key?(filter_schema.properties, "secret") + refute Map.has_key?(filter_schema.properties, "secret_calc") refute Map.has_key?(filter_schema.properties, :hidden_author) refute Map.has_key?(api_spec.components.schemas, "hidden-spec-post-filter-secret") refute Map.has_key?(api_spec.components.schemas, "hidden-spec-post-filter-secret_calc") @@ -694,35 +694,35 @@ defmodule Test.Acceptance.OpenApiTest do type: :deepObject, description: "Filters the query to results matching the given filter object", properties: %{ - author: %Reference{"$ref": "#/components/schemas/author-filter"}, - author_id: %Reference{ + :author => %Reference{"$ref": "#/components/schemas/author-filter"}, + "author_id" => %Reference{ "$ref": "#/components/schemas/post-filter-author_id" }, - count_of_tags: %Reference{ + "count_of_tags" => %Reference{ "$ref": "#/components/schemas/post-filter-count_of_tags" }, - email: %Reference{"$ref": "#/components/schemas/post-filter-email"}, - hidden: %Reference{"$ref": "#/components/schemas/post-filter-hidden"}, - id: %Reference{"$ref": "#/components/schemas/post-filter-id"}, - name: %Reference{"$ref": "#/components/schemas/post-filter-name"}, - and: %Schema{ + "email" => %Reference{"$ref": "#/components/schemas/post-filter-email"}, + "hidden" => %Reference{"$ref": "#/components/schemas/post-filter-hidden"}, + "id" => %Reference{"$ref": "#/components/schemas/post-filter-id"}, + "name" => %Reference{"$ref": "#/components/schemas/post-filter-name"}, + :and => %Schema{ type: :array, items: %Reference{ "$ref": "#/components/schemas/post-filter" }, uniqueItems: true }, - or: %Schema{ + :or => %Schema{ type: :array, items: %Reference{ "$ref": "#/components/schemas/post-filter" }, uniqueItems: true }, - name_twice: %Reference{ + "name_twice" => %Reference{ "$ref": "#/components/schemas/post-filter-name_twice" }, - not: %Reference{"$ref": "#/components/schemas/post-filter"} + :not => %Reference{"$ref": "#/components/schemas/post-filter"} }, additionalProperties: false, example: ""