Skip to content

Resolve types like we already do for enums - #26

Merged
tuckermcclure merged 3 commits into
mainfrom
tucker/when-base-module-isnt-main
Jun 23, 2026
Merged

tuckermcclure merged 3 commits into
mainfrom
tucker/when-base-module-isnt-main

Conversation

@tuckermcclure

Copy link
Copy Markdown
Member

This allows a package to read/write YAML/JSON using names relative to a given module rather than (implicitly) assuming everything is available from Main.

This is actually a small, non-breaking change. It's well tested and works with heavier downstream use cases.

See the updated readme.md.

@tuckermcclure
tuckermcclure requested a review from CarlOlsson June 18, 2026 23:37

@CarlOlsson CarlOlsson left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only comment from Codex, I guess not a blocker for this PR:

One round-trip edge case: binding_tag can write external module tags that resolve_name cannot load from the same base_module.

Example: if a package writes with base_module = MyPackage and an artifact contains a Dates.Date, the tag becomes:

type: Dates.Date

But loading with base_module = MyPackage tries to resolve that as MyPackage.Dates.Date, so it fails unless Dates is bound inside MyPackage.
So the new base_module behavior works for package-local types, but external dependency types may not round-trip unless those modules are imported into the base module. This probably needs either a resolver tweak or an explicit test/docs note for that limitation.

Base automatically changed from tucker/non-concrete-construction to main June 23, 2026 14:23
@tuckermcclure

Copy link
Copy Markdown
Member Author

The only comment from Codex, I guess not a blocker for this PR:

One round-trip edge case: binding_tag can write external module tags that resolve_name cannot load from the same base_module.

Example: if a package writes with base_module = MyPackage and an artifact contains a Dates.Date, the tag becomes:

type: Dates.Date

But loading with base_module = MyPackage tries to resolve that as MyPackage.Dates.Date, so it fails unless Dates is bound inside MyPackage. So the new base_module behavior works for package-local types, but external dependency types may not round-trip unless those modules are imported into the base module. This probably needs either a resolver tweak or an explicit test/docs note for that limitation.

Maybe I don't understand what you're saying here, but this is what the new unit test ("base_module type tags") is all about. There's an OtherModule.OtherType that's handed to a PackageArtifactFixtures.Wrapper struct (which can take Any). Then, we call PackageArtifactFixtures.Wrapper .write_yaml_artifact, which uses base_module = PackageArtifactFixtures.Wrapper. When it tries to write that type to YAML, it can't find it locally, so it writes it relative to Main, kind of like writing an absolute path when it can't resolve as a local path. On load, this does the same thing. module_path_from takes care of this type of behavior internally. So the take-away here is that you have to have the same types available (in the same arrangement) on load that you had on save, which is true for any serialization setup.

Here's a simpler example:

julia> import YAML

julia> import PortableStructs

julia> module A
           module B
               struct X; x::Float64; end
           end
       end;

julia> module C
       end;

julia> x = A.B.X(3.)
Main.A.B.X(3.0)

julia> PortableStructs.write_to_yaml("c.yaml", x; base_module = C);

julia> read("c.yaml", String) |> println
type: "Main.A.B.X"
x: 3.0

julia> PortableStructs.load_from_yaml("c.yaml"; base_module = C)
Main.A.B.X(3.0)

So that roundtrips perfectly. And just to be sure, you can also exit Julia, start a new one, define those same modules, and then call load_from_yaml and get the expected thing.

I think Codex missed on this one, or I'm misunderstanding the problem.

@tuckermcclure
tuckermcclure merged commit 398f3a2 into main Jun 23, 2026
6 checks passed
@tuckermcclure
tuckermcclure deleted the tucker/when-base-module-isnt-main branch June 23, 2026 14:46

CarlOlsson commented Jun 24, 2026 •

Copy link
Copy Markdown
Member

From Codex:

Thanks, I think your example and the new test cover the Main-root case correctly. The narrower case I meant is when the serialized type lives in a package/stdlib root module that is not nested under Main, Base, or Core, and is not bound inside the chosen base_module.

For example:

import YAML
import PortableStructs
import Dates

module C
end

x = Dates.Date(2026, 6, 24)

PortableStructs.write_to_yaml("c.yaml", x; base_module = C)
println(read("c.yaml", String))

PortableStructs.load_from_yaml("c.yaml"; base_module = C)

This writes a tag rooted at Dates, e.g.:

type: "Dates.Date"

But on load, resolve_name("Dates.Date"; base_module = C) starts from C, so it tries to find C.Dates and fails unless Dates is explicitly bound inside C.

So I agree that Main.A.B.X round-trips because resolve_name treats Main as an absolute root. The edge case is package-root tags like Dates.Date, where the writer emits a name that looks absolute to a human, but the resolver treats it as relative to base_module. If the intended contract is that external modules must be imported into the chosen base_module, then this is probably just a docs/test clarification rather than a blocker.

@tuckermcclure

Copy link
Copy Markdown
Member Author

Ok, good call. I believe this fixes it.

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.

2 participants