{"id":51111391,"url":"https://github.com/seeruk/morph","last_synced_at":"2026-06-24T18:01:06.750Z","repository":{"id":362992772,"uuid":"1232347395","full_name":"seeruk/morph","owner":"seeruk","description":"A Go tool for automatically generating mapping code for similar types.","archived":false,"fork":false,"pushed_at":"2026-06-14T12:18:34.000Z","size":336,"stargazers_count":12,"open_issues_count":0,"forks_count":1,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-06-14T16:13:17.175Z","etag":null,"topics":["codegen","go","golang"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/seeruk.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-07T20:56:15.000Z","updated_at":"2026-06-14T12:18:38.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/seeruk/morph","commit_stats":null,"previous_names":["seeruk/morph"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/seeruk/morph","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/seeruk%2Fmorph","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/seeruk%2Fmorph/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/seeruk%2Fmorph/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/seeruk%2Fmorph/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/seeruk","download_url":"https://codeload.github.com/seeruk/morph/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/seeruk%2Fmorph/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34743465,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-24T02:00:07.484Z","response_time":106,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["codegen","go","golang"],"created_at":"2026-06-24T18:01:05.555Z","updated_at":"2026-06-24T18:01:06.743Z","avatar_url":"https://github.com/seeruk.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Morph\n\nA Go tool for automatically generating mapping code for similar types.\n\nMorph is a CLI tool, but can also be used as a library, to be integrated into other applications as\npart of more complex code-generation pipelines.\n\nMorph generates functions like this:\n\n```go\nfunc MapFromRecipeToToRecipe(source *from.Recipe) to.Recipe {\n  if source == nil {\n    return to.Recipe{}\n  }\n  var target to.Recipe\n  target.ID = to.RecipeID(source.RecipeId)\n  target.Name = source.Name\n  target.Servings = int(source.Servings)\n  return target\n}\n```\n\nMorph can map structs and enums, including fields containing basic types, pointers, slices, arrays,\nmaps, nested structs, and concrete generic containers, using direct assignment, safe or configured\ntype conversions, generated nested mappers, and user-provided callables.\n\n## Why Morph?\n\nUsing certain libraries and tools can mean you to end up with what are essentially duplicated types.\nFor example, the ProtoBuf compiler doesn't generate idiomatic Go code, so you may want to represent\nthe same types with idiomatic Go code (e.g. using `time.Time`, with correct initialism in field\nnames, so on), or maybe you have a database library which uses code-gen.\n\nMorph exists to attempt to alleviate the burden of writing boring, error-prone, time-consuming\nmanual mapping code for these types.\n\n## Quick Start\n\nInstall Morph using the Go toolchain:\n\n```bash\n$ go install github.com/seeruk/morph/cmd/morph@latest\n```\n\nMorph requires a configuration file to get started. You can find more about that in the\n[Configuration Overview](#configuration-overview) section below.\n\nOnce you have a valid configuration file, you can run Morph.\n\nIf you have a `morph.yaml` in the same folder:\n\n```bash\n$ morph\n```\n\nThis is equivalent to the explicit generate command:\n\n```bash\n$ morph generate\n```\n\nIf you want to point Morph at a specific configuration file:\n\n```bash\n$ morph -config path/to/config.yml\n```\n\nYou can preview what Morph would write without changing files:\n\n```bash\n$ morph --dry-run\n$ morph generate --dry-run\n```\n\nMorph plans and generates code based on where the config file is. The configuration file must be\nwithin a Go module.\n\n## Configuration Overview\n\nMorph requires a configuration file to function. It does not support taking parameters as flags. A\nvery basic configuration file to map between a few types in a couple of packages could look like\nthis:\n\n```yaml\n# yaml-language-server: $schema=https://raw.githubusercontent.com/seeruk/morph/main/schemas/config.schema.json\npackages:\n- source: example.com/foodplanner/foodpb\n  target: example.com/foodplanner/food\n  types:\n  - name: Recipe\n  - name: Ingredient\n  - source: Difficulty\n    target: RecipeDifficulty\n```\n\nConfiguration allows you to control quite a lot about how mapping works, what is generated, where it\ngets generated, and what other resources Morph can draw on.\n\nThe following sections cover other config sections, and following that are some other common\n\"recipes\" for things you might want to be able to do with Morph.\n\n\u003cdetails\u003e\n\u003csummary\u003eDefaults Hierarchy\u003c/summary\u003e\n\n### Defaults Hierarchy\n\nMorph configuration is layered, allowing you to specify defaults, and subsequently override them at\nmore granular levels. Morph aims to be an unopinionated tool with sensible defaults. Top-level\ndefaults are specified in the `defaults` section of the configuration.\n\nThe order of preference is:\n\n1. Property-level config\n2. Type-level config\n3. Type preset\n4. Package-level config\n5. Package preset\n6. Top-level default config\n7. Morph built-in defaults\n\nIt's worth noting, configuration on a package, type, or property level does not trickle down to nested\nmapping functions that Morph generates automatically. If you need Morph to make a customized mapper,\nit must be specified in the config file, or use top-level defaults.\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003eConversions\u003c/summary\u003e\n\n### Conversions\n\nMorph supports generating type conversions between basic types when it's safe to do so. This\nbehaviour can be extended through configuration, allowing unsafe basic conversions, and allowing\ncustom types to be converted if their underlying type supports it.\n\nConversions are configured at the top-level in configuration:\n\n```yaml\nconversions:\n- source: int\n  targets:\n  - int64\n  - uint64\n  bidirectional: true\n- source: StringBasedID\n  targets:\n  - string\n```\n\nAs conversions are global configuration, you might find there are scenarios where you want to\ndisable them for certain packages, types, or properties. This can be done at any of these levels like\nso:\n\n```yaml\npackages:\n- source: example.com/source\n  target: example.com/target\n  conversions:\n    enabled: false # Disable for this package pair.\n  types:\n  - name: Example\n    conversions:\n      enabled: true # Re-enable for this type pair.\n    struct:\n      properties:\n      - name: LegacyID\n        conversions:\n          enabled: false # Disable again for this property.\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003eDiscovery\u003c/summary\u003e\n\n### Discovery\n\nMorph supports automatically finding and using potentially compatible mapping functions. This\nfunctionality is separate from explicitly asking Morph to use callables for mapping, and allows\nMorph to automatically use functions from explicitly listed packages, like so:\n\n```yaml\ndiscovery:\n  packages:\n  - github.com/example/mappers/datetime\n  - github.com/example/mappers/numeric\n  exclusions:\n  - github.com/example/mappers/numeric.IntToInt64\n```\n\nExclusions can be provided to prevent Morph from using specific functions discovered in these\npackages, which can be useful if there are many potential functions, and not all of them are\nactually intended for use as mapping functions.\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003eCallables\u003c/summary\u003e\n\n### Callables\n\n#### What are Callables?\n\nCallables are functions or methods that can be explicitly referenced in the config file for Morph to\npotentially use for mapping, instead of Morph generated the mapping itself. There are 2 main kinds\nof callables:\n\n##### Plain Callables\n\nPlain callables are simple functions which take a source type and return a target type. These\ncallables can error, and if they do, that errability will propagate up to the parent mapper it's\nused in, and so on.\n\n```go\nfunc FooToBar(foo Foo) Bar\nfunc FooToBarE(foo Foo) (Bar, error)\n```\n\nMorph does also support generic callables, as long as they're used on matching concrete types. For\nexample. You might have an `Optional[T any]` and a `Nullable[T any]`, and they might be used on a\nsource field like `Foo Optional[string]` to `Foo Nullable[string]` - this is fine, and works pretty\nmuch the same as above:\n\n```go\nfunc OptionalToNullable[T any](o Optional[T]) Nullable[T]\nfunc OptionalToNullable[T any](o Optional[T]) (Nullable[T], error)\n```\n\nThere are potential generic cases where Morph cannot use these functions though, for example, if the\ntype arguments differ on the source and target type (`Foo Optional[Bar]` to `Foo Nullable[Qux]`). In\nthis case, Morph wouldn't be able to map the inner type argument, it has no way to control it. For\nthese kinds of cases, you can use a combinator callable.\n\n##### Combinator Callables\n\nCombinator callables allow you to provide callables to Morph which can be used to handle many\ngeneric types. They look like this:\n\n```go\nfunc OptionalToNullable[I, O any](o Optional[I], mapFn func(I) O) Nullable[O]\nfunc OptionalToNullableE[I, O any](o Optional[I], mapFn func(I) (O, error)) (Nullable[O], error)\n```\n\nMorph can pass mapping functions it uses, or generates, or can generate inline mapping functions to\npass to these callables. If there are multiple type parameters, Morph expects a mapping function\nargument on the callable for each type parameter on the source/target type; for example, for an\n`Either[L, R any]` to `Tuple[A, B]` conversion, you could have:\n\n```go\nfunc EitherToTuple[L, R, A, B any](\n    e Either[L, R],\n    mapLeft func(L) A,\n    mapRight mapRight func(R) B,\n) Tuple[A, B]\n```\n\nThe mapping functions should look like plain callables, and each mapping function argument may\nreturn an error.\n\n#### Configuring Callables\n\nThe aforementioned discovery is only for auto-discovery of entire packages worth of functions, for\nother callables to be used by Morph, you must specify them explicitly. Discovery is a nice way to\ninclude packages designed specifically for mapping, but you could end up pulling in way more than\nyou want. Also, discovery is not scoped.\n\nExplicitly configuring callables is the solution to both of those issues. Similar to other\nconfiguration options, you can configure callables in defaults, presets, on packages, on types, and\non specific properties. Configuration looks something like this:\n\n```yaml\npackages:\n- source: example.com/foodplanner/foodpb\n  target: example.com/foodplanner/food\n  types:\n  - name: Recipe\n    callables:\n    - google.golang.org/protobuf/types/known/timestamppb.Timestamp.AsTime\n    - google.golang.org/protobuf/types/known/timestamppb.New\n```\n\nIn the above example, since this is specified at the type level, these functions can be used by\nMorph for any property's value mapping. It will not trickle down to nested mappings.\n\nScoped callables are prioritized by where they are configured: type callables are tried before type\npreset callables, then package callables, package preset callables, and finally defaults. Within the\nsame priority, Morph uses callable compatibility rank to choose the best candidate.\n\nSpecifying callables in the `defaults` section will make the callables available to any mapper at\nthe lowest priority.\n\nProperty callables can also receive ordered context source arguments. Context arguments must be exact\nsource fields or zero-argument methods; Morph does not infer or strip accessor prefixes for them.\n\n```yaml\npackages:\n- source: example.com/foodplanner/foodpb\n  target: example.com/foodplanner/food\n  types:\n  - name: Recipe\n    struct:\n      properties:\n      - name: Title\n        callable:\n          forward:\n            ref: example.com/foodplanner/food.OmittableFromPresence\n            args:\n            - source: MorphHasTitle\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003ePresets\u003c/summary\u003e\n\n### Presets\n\nMorph allows you to write named collections of default configuration which can be applied at the\npackage or type level. If you have a common pattern you want to use for certain packages, then it\nmeans you can drastically cut down on duplicate config. Presets can be defined as so:\n\n```yaml\npresets:\n  protobuf:\n    bidirectional: true\n    callables:\n    - google.golang.org/protobuf/types/known/timestamppb.Timestamp.AsTime\n    - google.golang.org/protobuf/types/known/timestamppb.New\n\n    enum:\n      failureMode: error\n      patterns:\n        source: \"{{ .Type.Pascal }}_{{ .Type.Screaming }}_{{ .Value.Screaming }}\"\n        target: \"{{ .Type.Pascal }}{{ .Value.Pascal }}\"\n\n    mappers:\n      forward:\n        name: Map{{ .Target.Type }}FromProto\n        signature:\n          accepts: pointer\n          returns: value\n      inverse:\n        name: Map{{ .Target.Type }}ToProto\n        signature:\n          accepts: value\n          returns: pointer\n\n    optionality:\n      onNilSourcePointer: zero\n      onZeroSourceValue: nil\n\n# And then applied:\npackages:\n- source: example.com/foopb\n  target: example.com/foo\n  preset: protobuf\n  types:\n  - name: Bar\n    # Or at the specific type level\n    preset: protobuf\n```\n\n\u003c/details\u003e\n\n### Other Common Scenarios\n\n\u003cdetails\u003e\n\u003csummary\u003eConfiguring Output\u003c/summary\u003e\n\n#### Configuring Output\n\nBy default, Morph generates code into a single `mapping` package, in a `mapping` directory next to\nthe configuration file. You can configure this globally:\n\n```yaml\ndefaults:\n  packages:\n    output:\n      strategy: single_package\n      path: internal/mapping\n      package: mapping\n      filename: mapping.morph.go\n```\n\nOr, for a specific package mapping:\n\n```yaml\npackages:\n- source: example.com/foodplanner/foodpb\n  target: example.com/foodplanner/food\n  output:\n    strategy: target_package\n    filename: mapping.morph.go\n  types:\n  - name: Recipe\n```\n\nThere are 3 output strategies:\n\n* `single_package` writes generated code to the configured `path` and `package`.\n* `source_package` writes generated code into the source package.\n* `target_package` writes generated code into the target package.\n\nFor `source_package` and `target_package`, only `filename` is used. The package name and path come\nfrom the existing package Morph is writing into.\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003eCustomizing Mapper Function Names\u003c/summary\u003e\n\n#### Customizing Mapper Function Names\n\nMorph generates mapper function names from templates. You can configure mapper names in defaults,\npresets, on packages, or on individual types. If you're generating ProtoBuf mappings, for example,\nyou might want names which make the direction clearer:\n\n```yaml\npackages:\n- source: example.com/foodplanner/foodpb\n  target: example.com/foodplanner/food\n  mappers:\n    forward:\n      name: Map{{ .Target.Type }}FromProto\n    inverse:\n      name: Map{{ .Target.Type }}ToProto\n  bidirectional: true\n  types:\n  - name: Recipe\n```\n\nWith the above config, Morph would generate names like `MapRecipeFromProto` and\n`MapRecipeToProto`.\n\nPatterns use Go's `text/template` library. Input to the template is `nameTemplateData` found in\n[naming.go](naming.go#L23). The package values are the Go package names with the first letter\nuppercased, and the signature values are rendered as `Value` or `Pointer`.\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003eCustomizing Mapper Kind\u003c/summary\u003e\n\n#### Customizing Mapper Kind\n\nBy default, Morph generates plain functions. You can ask Morph to generate source-type methods\ninstead:\n\n```yaml\npackages:\n- source: example.com/foodplanner/foodpb\n  target: example.com/foodplanner/food\n  output:\n    strategy: source_package\n  mappers:\n    forward:\n      kind: prefer_method\n  types:\n  - name: Recipe\n```\n\n* `kind: function` preserves the default function output.\n* `kind: prefer_method` emits a method when the generated file is in the source type's package, and \n  otherwise falls back to a function.\n* `kind: method` will emit a fatal diagnostic if Morph can't generate a method.\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003eCustomizing Mapper Signatures\u003c/summary\u003e\n\n#### Customizing Mapper Signatures\n\nSimilar to configuring mapper names, you can customize the signature of a mapper, controlling\nwhether the function accepts/returns pointers/values:\n\n```yaml\npackages:\n- source: example.com/foodplanner/foodpb\n  target: example.com/foodplanner/food\n  types:\n  - name: Recipe\n    mappers:\n      forward:\n        name: Map{{ .Source.Type }}PointerTo{{ .Target.Type }}\n        signature:\n          accepts: pointer\n          returns: value\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003eOverriding Property / Enum Value Mapping\u003c/summary\u003e\n\n#### Overriding Property / Enum Value Mapping\n\nMorph will try to match logical struct properties by name, including case-insensitive matches. A\nproperty is usually backed by a Go field, but can also be backed by getter and setter methods. If\nproperty names don't match clearly, you can map them explicitly:\n\nWhen `inferMethods` is enabled, Morph can use getter and setter-shaped methods as mapping\ncandidates, but unused inferred methods do not produce unmapped-property warnings. Coverage\nwarnings are reserved for field-backed properties.\n\n```yaml\npackages:\n- source: example.com/foodplanner/foodpb\n  target: example.com/foodplanner/food\n  types:\n  - name: Recipe\n    struct:\n      properties:\n      - source: RecipeId\n        target: ID\n```\n\nYou can also configure exact accessors when Morph should read or write through specific methods:\n\n```yaml\npackages:\n- source: example.com/foodplanner/foodpb\n  target: example.com/foodplanner/food\n  types:\n  - name: Recipe\n    struct:\n      properties:\n      - source: EmailAddress\n        target: Email\n        accessors:\n          forward:\n            read: GetEmailAddress\n            write: SetEmail\n```\n\nEnums work similarly. Morph will try to infer enum mappings by normalizing names, but you can\nprovide explicit value mappings where names don't line up:\n\n```yaml\npackages:\n- source: example.com/foodplanner/foodpb\n  target: example.com/foodplanner/food\n  types:\n  - source: Difficulty\n    target: RecipeDifficulty\n    enum:\n      failureMode: error\n      values:\n        Difficulty_DIFFICULTY_UNSPECIFIED: RecipeDifficultyUnknown\n```\n\nBy default, enum mappers return an error when the source value falls through the generated switch.\nSet `failureMode: zero` to return the target enum's zero value instead, or `failureMode: fallback`\nto return a configured target enum constant. Fallback mode also allows inferred source constants\nwith no target match; those constants are omitted from the switch and use the fallback at runtime.\nExplicit `values` entries are still validated.\n\n```yaml\npackages:\n- source: example.com/foodplanner/foodpb\n  target: example.com/foodplanner/food\n  types:\n  - source: Difficulty\n    target: RecipeDifficulty\n    enum:\n      failureMode: fallback\n      fallback:\n        forward: RecipeDifficultyUnknown\n        inverse: Difficulty_UNSPECIFIED\n```\n\nFor enums with regular generated naming patterns, you can also configure patterns instead of\nlisting every value:\n\n```yaml\npackages:\n- source: example.com/foodplanner/foodpb\n  target: example.com/foodplanner/food\n  types:\n  - source: Difficulty\n    target: RecipeDifficulty\n    enum:\n      patterns:\n        source: \"{{ .Type.Pascal }}_{{ .Type.Screaming }}_{{ .Value.Screaming }}\"\n        target: \"{{ .Type.Pascal }}{{ .Value.Pascal }}\"\n```\n\nPatterns use Go's `text/template` library. Input to the template is `enumTemplateData` found in\n[planner_enum.go](planner_enum.go#L259).\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003eOmitting Properties\u003c/summary\u003e\n\n#### Omitting Properties\n\nMorph reports coverage warnings when a target property cannot be populated from a source property,\nor vice versa. If a property is intentionally outside the mapping, you can omit it like so:\n\n```yaml\npackages:\n- source: example.com/foodplanner/foodpb\n  target: example.com/foodplanner/food\n  bidirectional: true\n  types:\n  - name: Recipe\n    struct:\n      omit:\n        both:\n        - Name\n        source:\n        - InternalState\n        target:\n        - CreatedAt\n        - UpdatedAt\n```\n\nUse `both` when the same logical property exists on both sides but should not be mapped. Use `source`\nfor source-only properties that should be ignored, and `target` for target-only properties that should\nbe left unset.\n\nFor bidirectional mappings, source and target omissions are inverted automatically. Properties listed\nunder `source` are treated as target omissions on the inverse mapper, and properties listed under\n`target` are treated as source omissions on the inverse mapper. Properties listed under `both` remain\nmatched omissions in both directions.\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003eBidirectional Mapping\u003c/summary\u003e\n\n#### Bidirectional Mapping\n\nMany mappings are useful in both directions. You can enable bidirectional mapping in defaults, in a\npreset, on a package, or on a specific type:\n\n```yaml\npackages:\n- source: example.com/foodplanner/foodpb\n  target: example.com/foodplanner/food\n  bidirectional: true\n  types:\n  - name: Recipe\n  - source: Difficulty\n    target: RecipeDifficulty\n```\n\nThis will generate both `foodpb -\u003e food` and `food -\u003e foodpb` mappings. Struct property mappings and\nenum value mappings are inverted automatically for the inverse mapper. Enum fallback values are\nconfigured directionally because each generated mapper returns a different target enum type.\n\nIf only one type should be bidirectional, configure it at the type level:\n\n```yaml\npackages:\n- source: example.com/foodplanner/foodpb\n  target: example.com/foodplanner/food\n  types:\n  - name: Recipe\n    bidirectional: true\n```\n\n\u003c/details\u003e\n\n## Known Limitations\n\n* Morph only generates top-level mappers for struct-to-struct or enum-to-enum mappings and does not\n  support generating mapping functions for other types (e.g. basic types, slices, maps, so on).\n* Morph does not load test packages, so cannot create mappings for types in test files.\n* Morph does not support embedded fields.\n* Morph only recognizes the standard, built-in `error` type for discovery, not custom aliases or\n  wrappers.\n* Morph does not support creating mappers explicitly for generic types. See\n  [docs/decisions/01-high-order-explicit-roots.md][1] for the rationale.\n* Morph assumes at least one package referenced in the spec is the main module. If this is not the\n  case, Morph will not be able to figure out the workspace and planning will fail.\n* If using `single_package` output, package name detection includes files that have build\n  constraints, which could mean either the package name is incorrect, or that an error is returned\n  when it shouldn't be.\n\n## Future Enhancements\n\n* Assignment of literal / constant values for unmapped fields (i.e. while mapping set field x to y)\n* CLI improvements:\n  * `morph plan` / `morph explain`, some sort of human-readable and/or machine-readable plan view\n  * `morph init`, maybe point it at packages or something? Or maybe a different command which\n    creates or updates config to include pairs of types found? `morph scan` or something?\n* Package-local helpers could support cross-package mappings involving unexported fields.\n* Built-in helpers which can be used for discovery en masse\n\n## License\n\nMIT\n\n[1]: docs/decisions/01-high-order-explicit-roots.md\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fseeruk%2Fmorph","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fseeruk%2Fmorph","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fseeruk%2Fmorph/lists"}