Skip to content

fix: AshGeo.GeoJson.cast_stored handles GeoJSON maps#14

Open
matt-beanland wants to merge 1 commit into
bcksl:mainfrom
matt-beanland:fix/geojson-cast-stored-maps
Open

fix: AshGeo.GeoJson.cast_stored handles GeoJSON maps#14
matt-beanland wants to merge 1 commit into
bcksl:mainfrom
matt-beanland:fix/geojson-cast-stored-maps

Conversation

@matt-beanland

@matt-beanland matt-beanland commented May 27, 2026

Copy link
Copy Markdown

Problem

AshGeo.GeoJson.cast_input/2 decodes GeoJSON-shaped maps (atom- or string-keyed) via Geo.JSON.decode/1:

def cast_input(%{type: _} = value, constraints) do
  with {:ok, value} <- Jason.encode(value),
       {:ok, value} <- Jason.decode(value),
       {:ok, value} <- Geo.JSON.decode(value) do
    {:ok, value}
  else
    _ -> :error
  end
end

def cast_input(value, constraints) when is_map(value) and not is_struct(value) do
  case Geo.JSON.decode(value) do
    {:ok, _} = res -> res
    _ -> :error
  end
end

But cast_stored/2 is not overridden in AshGeo.GeoJson.Use, so it falls through to the base AshGeo.Geometry implementation, which only accepts %Geo.*{} structs and nil:

def cast_stored(%struct{} = value, _) when is_geo(struct), do: {:ok, value}
def cast_stored(nil, _), do: {:ok, nil}
def cast_stored(_, _), do: :error   # <- maps land here

This breaks any data layer that stores GeoJSON as a map/JSON value and hands it back as a map on read. The case that bit me: an AshGeo.GeoJson field nested inside an Ash.TypedStruct. When the TypedStruct walks its fields on cast_stored, the geo field is handed the decoded GeoJSON map and AshGeo.GeoJson.cast_stored returns :error, failing the whole load — even though cast_input would have accepted the identical map.

Fix

Add map-handling cast_stored/2 clauses to AshGeo.GeoJson.Use mirroring the existing cast_input/2 clauses — same Jason round-trip for atom-keyed maps, same Geo.JSON.decode for string-keyed maps, falling through to super/2 for structs/nil/binaries.

Tests

Adds cast_stored cases for atom-keyed map, string-keyed map, and nil, alongside the existing Geo-struct case. Verified locally that Geo struct / nil / binary-rejection behaviour is unchanged.


Discovered while adopting AshGeo as the spatial type surface for ash_neo4j (a Neo4j Ash.DataLayer), which stores geometries as RFC 7946 GeoJSON. Pairs with #13 (the bare-atom geo_types formatter crash). Happy to adjust to fit your preferences.

Summary by CodeRabbit

  • Bug Fixes

    • Fixed improper handling of GeoJSON deserialization from storage when GeoJSON values are nested within other typed data structures, ensuring they deserialize correctly.
  • Tests

    • Added test coverage validating GeoJSON deserialization from stored data with various input formats.

Review Change Stack

`cast_input/2` decodes GeoJSON-shaped maps (atom- or string-keyed) via
`Geo.JSON.decode/1`, but `cast_stored/2` falls through to the base
`AshGeo.Geometry` implementation, which only accepts `%Geo.*{}` structs
and `nil` — it rejects maps with `:error`.

That breaks any data layer that stores GeoJSON as a map/JSON value, and
in particular any containing type whose nested `AshGeo.GeoJson` field
deserialises to a map on read. For example an `AshGeo.GeoJson` field
inside an `Ash.TypedStruct`: when the struct walks its fields on
cast_stored, the geo field is handed the decoded map and
`AshGeo.GeoJson.cast_stored` returns `:error`, failing the load.

Adds map-handling `cast_stored/2` clauses to `AshGeo.GeoJson.Use`
mirroring the existing `cast_input/2` clauses (same Jason-roundtrip for
atom-keyed maps, same `Geo.JSON.decode` for string-keyed). Geo structs,
nil, and the binary-rejection behaviour are unchanged.

Tests: cast_stored from atom-keyed map, string-keyed map, and nil.
@coderabbitai

coderabbitai Bot commented May 27, 2026

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 785fded5-5130-4bd7-97e8-accd963c9f43

📥 Commits

Reviewing files that changed from the base of the PR and between c002217 and 30058da.

📒 Files selected for processing (2)
  • lib/ash_geo/geo_json.ex
  • test/ash_geo_test.exs

📝 Walkthrough

Walkthrough

This PR adds cast_stored/2 implementations to AshGeo.GeoJson.Use that properly deserialize GeoJSON maps from storage by JSON round-tripping through Jason and decoding via Geo.JSON, with corresponding test coverage for atom-keyed maps, string-keyed maps, and nil inputs.

Changes

GeoJSON Stored Value Deserialization

Layer / File(s) Summary
GeoJSON stored value deserialization implementation and tests
lib/ash_geo/geo_json.ex, test/ash_geo_test.exs
cast_stored/2 clauses for typed-maps (%{type: _}) and non-struct maps use Jason.encode/1, Jason.decode/1, and Geo.JSON.decode/1 to deserialize stored GeoJSON values, with test cases verifying correct handling of atom-keyed maps, string-keyed maps, and nil inputs.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~8 minutes

Poem

A rabbit hops through JSON streams,
Where GeoJSON dreams take shape with themes,
Maps decode from storage's vault so deep,
Round-tripping through the data's keep,
Geo.JSON makes the coordinates leap! 🐰✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main fix: adding support for GeoJSON maps in cast_stored/2, which is the core change across both modified files.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@matt-beanland

Copy link
Copy Markdown
Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 28, 2026

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant