Skip to content

feat: serialize pathlib paths like the other extra types - #1962

Closed
shcheklein wants to merge 1 commit into
mainfrom
fix/json-encode-path
Closed

feat: serialize pathlib paths like the other extra types#1962
shcheklein wants to merge 1 commit into
mainfrom
fix/json-encode-path

Conversation

@shcheklein

@shcheklein shcheklein commented Aug 29, 2026

Copy link
Copy Markdown
Contributor

A model field declared as a Path cannot be stored:

class Doc(dc.DataModel):
    source: pathlib.PurePosixPath

class Batch(dc.DataModel):
    docs: list[Doc]

dc.read_values(b=[Batch(docs=[Doc(source=pathlib.PurePosixPath("/data/a.txt"))])]).save("b")
# TypeError: Object of type PurePosixPath is not JSON serializable

datachain.json exists to teach ujson the types it does not handle on its own — datetime, date, time, UUID, numpy arrays, bytes. Paths were missing, so a value that reaches the encoder still holding its Python type has nowhere to go.

That happens for a list of models, whose fields are dumped in Pydantic's python mode and therefore keep their Python types. The same model reached another way is converted by Pydantic first and stores fine, which is why this shows up in one shape and not another.

Paths are written with str(), which is what Pydantic's JSON mode produces for PurePath, PurePosixPath and Path alike, so a value written either way reads back identically. The test asserts that equivalence against model_dump(mode="json") rather than hard-coding a string.

Scope: values, not keys

Teaching the encoder about paths made one shape reachable that main rejected, and it stored badly — the key became its JSON text, quotes and all, the two read routes disagreed, and an equality filter matched nothing. So a path used as a mapping key is refused explicitly where _to_jsonable builds the mapping, keeping main's behaviour:

TypeError: Cannot use PurePosixPath as a mapping key: paths are not
supported as JSON object names

This covers a path key wherever _to_jsonable builds the mapping, which is every JSON column on both backends. It does not reach array items, whose handling already differs by backend for reasons unrelated to paths — that is untouched here.

Making rich mapping keys work — paths, datetimes, UUIDs — needs read-side reconstruction and filter normalisation, and none of them work today. That belongs with the read-path work in #1918.

Both encoder branches are covered: ujson silently coerces a path key while stdlib rejects it, so the tests assert path values work under serialize_bytes either way.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 29, 2026

Copy link
Copy Markdown

Deploying datachain with  Cloudflare Pages  Cloudflare Pages

Latest commit: 934e119
Status: ✅  Deploy successful!
Preview URL: https://3d3b2bef.datachain-2g6.pages.dev
Branch Preview URL: https://fix-json-encode-path.datachain-2g6.pages.dev

View logs

@codecov

codecov Bot commented Aug 29, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@shcheklein
shcheklein force-pushed the fix/json-encode-path branch 3 times, most recently from 97ba22e to 9a8c415 Compare August 29, 2026 21:59
datachain.json already teaches ujson the types it does not handle on its own --
datetime, date, time, UUID, numpy arrays, bytes -- so that values reaching it as
Python objects can still be written. A path was not among them, so a model field
declared as Path fails to serialize once its value arrives unconverted:

    TypeError: Object of type PurePosixPath is not JSON serializable

That is reachable today through a list of models, whose fields are dumped in
Pydantic's python mode and so keep their Python types.

Paths are written with str(), which is the spelling Pydantic's JSON mode
produces for PurePath, PurePosixPath and Path alike, so a value written through
either route reads back the same.

Paths stay refused as mapping keys where _to_jsonable builds the mapping, which
is what happened before by way of the encoder not knowing the type. A key is
written as its JSON text, quotes and all, and nothing on the read side turns
that back into a path, so accepting one would store a name no query matches.

Array items are a separate matter and are left alone: whether one reaches
_to_jsonable at all already differs by backend, which has nothing to do with
paths. Rich mapping keys need read-side reconstruction to work anywhere, for
paths as much as for datetimes, and that is left alone here too.
@shcheklein
shcheklein force-pushed the fix/json-encode-path branch from 9a8c415 to 934e119 Compare August 29, 2026 22:27
@shcheklein

Copy link
Copy Markdown
Contributor Author

Closing. Path was never the goal here — it entered as a regression of my own change.

The chain was: #1943's xfail needs live models to reach the converter as plain dicts, so I proposed flattening tuples like lists; that used python-mode model_dump(), which leaves a Path as a Path, which broke tuple[Model-with-Path, ...] — a shape that works on main because the warehouse converts it in JSON mode. This PR existed to stop that regression. That flatten change has since been dropped, so nothing depends on this.

It also does not earn its place on its own. Measured against main:

shape main this PR
Path as a direct field ERR ERR
list[Path] ERR ERR
dict[str, Path] (value) ERR saves, reads back a str
dict[Path, str] (key) ERR ERR
model w/ Path field ERR ERR
list[model w/ Path] ERR ok
tuple[model w/ Path,...] ok ok

One row fixed, one row turned from a loud failure into silent type loss. Not a good trade for three lines.

The useful finding is why: type support is spread across python_to_sql, the JSON encoder, the read converters, _convert_feature_value, and the key-encoding rules, each with its own list and no shared registry. list[model w/ Path] works only because pydantic rebuilds it from the model annotation; dict[str, Path] does not, because that reconstruction is DataChain's own and has never heard of Path. Same type, different layer, opposite result. Filing that separately.

@shcheklein shcheklein closed this Aug 29, 2026
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