geojsonkit.org
GeoJSON utilities, in the browser
Say hi →

Join a CSV table to GeoJSON

The boundaries came from one place and the numbers came from another. This is the step in between: match every feature to a row by a key they share, and copy the columns you want onto the feature's properties. It also tells you what failed to match, which is the part a silent join never does.

Join CSV to GeoJSON

updated 7 September 2026

Drop a .geojson file, or
Drop a .csv or .tsv file, or

What this tool does

It performs an attribute join: for every feature it looks up one property value in a CSV table and copies that row's other columns onto the feature. Geometry is never touched — only properties grows.

The intent it closes: "I have the shapes and I have the numbers, and they are in two different files." That is the normal state of affairs. Boundary files are published once and carry a code and little else; the thing you actually want to map — population, revenue, turnout, case counts — arrives later as a spreadsheet keyed by that same code. Desktop GIS calls this a table join and puts it four dialogs deep. Here it is two panes and a pair of pickers.

How this tool works

The table is read once and indexed by the key column, so the join is a hash lookup per feature rather than a scan — a 5,000-feature file against a 5,000-row table is 5,000 lookups, not 25 million comparisons.

Both key pickers are filled from the files you paste, never typed from memory. That is deliberate: a key name that does not exist matches nothing and produces a perfectly valid file with no new columns in it, which is the worst possible failure because it looks like success. If the property list and the column list disagree, you can see it before you run anything.

Rows are indexed by first occurrence. If two rows share a key the second is skipped and counted, because there is no correct way to attach two different populations to one district — that table needs filtering or grouping first, and you should know rather than get one of them at random.

Options

Feature property / Table column

The two sides of the match. They do not have to be named the same thing — iso_a2 against country_code is fine. The picker guesses: a column with the same name as the chosen property wins, otherwise something that looks like an id or a code, otherwise the first one.

Loose key match

On, keys are trimmed and compared without case, so "UA-30 " matches "ua-30". This is on by default because trailing spaces out of Excel are the most common reason a join that should work does not. Turn it off when case is meaningful — two distinct codes differing only in case do exist in some national schemes.

Copy columns

Blank copies everything except the key column itself (it is already on the feature). Name columns to copy just those — pop,area — and the key column is copied too if you ask for it by name.

Prefix

Prepended to every copied column name. Use it when the table and the features share a column name you want to keep both of: csv_name next to name.

Overwrite existing properties

Off, a column that would land on a property that already exists is skipped and counted in the status line, so nothing on your features is destroyed by accident. On, the table wins.

Convert numbers and booleans

CSV has no types; every cell arrives as text. On, a cell that reads as a number becomes a JSON number and true/false/null become those literals — which is what a renderer's colour scale needs. Identifiers are protected: 007, +44 and a twenty-digit account number stay strings, because they do not survive the round trip through a float.

Output

Every feature keeps the collection intact, tagged where it matched. Only matched is the inner join. Only unmatched is the diagnostic view — the features whose key is missing from the table, which is how you find the seven districts spelled differently.

Example

Two features and a table keyed by code:

{"type":"FeatureCollection","features":[
 {"type":"Feature","properties":{"code":"UA-30"},"geometry":null},
 {"type":"Feature","properties":{"code":"UA-32"},"geometry":null}]}
code,name,population
UA-30,Kyiv,2952301
UA-46,Lviv,724314

Output, with every feature selected:

{"type":"FeatureCollection","features":[
 {"type":"Feature","properties":{"code":"UA-30","name":"Kyiv","population":2952301},"geometry":null},
 {"type":"Feature","properties":{"code":"UA-32"},"geometry":null}]}

The status line reads 1 of 2 features matched · 1 feature matched nothing · 2 columns copied · 1 table row never used. Both halves of the mismatch are visible: a feature with no data, and a row of data with no feature. That is the pair of numbers to check before you publish a map.

Tips & common pitfalls

  • Leading zeros are the classic join killer. A FIPS code of 01001 becomes 1001 the moment the table is opened in Excel and saved. If everything matched except the low-numbered ones, that is what happened — re-export with the column typed as text.
  • Check "table rows never used" as carefully as the match count. A high number there means the table has keys your geometry does not, which usually means the two files are from different vintages of the same boundary set.
  • Nothing matched at all? Look at the two keys side by side. "UA-30" against "UA30", a code against a name, or a numeric id stored as 30 on one side and "30" on the other — the loose option handles the last of those, since both are compared as text.
  • The join is one row per feature. For one-to-many data — several readings per station — aggregate the table first; otherwise all but the first reading are skipped as repeated keys.
  • No shared key at all? If the table has coordinates instead, build features from it with CSV to GeoJSON. If the relationship is spatial rather than by key — points falling inside districts — use point in polygon, which is the other kind of join.

FAQ

Does it handle TSV or semicolon files?

Yes. The delimiter is detected from the header line, and you can force comma, tab, semicolon or pipe. Quoted fields, embedded delimiters, doubled quotes and CRLF line endings are all parsed properly, so a European export with ; separators works as-is.

Are my files uploaded anywhere?

No. Both panes are read in the page and the join runs in your browser. Nothing is sent to a server, which matters here because the table half of a join is usually the sensitive half.

What happens to features with no geometry?

They are kept and joined like any other. A null geometry is legal GeoJSON, and the attributes are the point of this tool.

Can I join on two columns at once?

Not directly. Build a single composite key first — with filter features for the GeoJSON side, or a spreadsheet formula for the table — then join on that. A concatenated state|county is the usual approach.

How large a file can it take?

Tens of thousands of features and rows are comfortable; the ceiling in practice is the size of the text your browser will hold in two textareas, and both files being fully in memory at once. Use the file pickers rather than pasting for anything above a few megabytes.