Library / SDK
spf13/cast avatar
spf13/cast

cast: Go type conversion that tells you when it failed

safe and easy casting from one type to another in Go

3,981 stars339 forksGoMIT

At a glance

What is it?
spf13/cast gives every Go conversion two method forms, one that returns a zero value on failure and one that returns an error. A look at what it converts, what it refuses to guess, and where the generated alias set fits.
Who is it for?
Reach for cast when your input arrives as `interface{}` from YAML, TOML, JSON or a plugin boundary and you want one conversion path instead of a switch statement of type assertions. Keep using plain Go conversion when both sides are statically typed, because a library call there buys nothing.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 32 days ago.
What is it written in?
Mainly Go, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 11, 2026, and from our analysis. They are not legal advice.

Editorial analysis

Two method families for every conversion

The central design decision is a doubled API. Every conversion exists as `ToString` and `ToStringE`, `ToInt` and `ToIntE`, and so on across the whole set. The first family always returns the desired type, and the README is explicit that when the input will not convert, you get the zero value for that type. The second family returns that same value plus an error, which is the only way to tell a genuine zero in the input from a failed conversion.

That distinction is the reason to use the library at all. A configuration value of `port: 0` and a port field that arrived as the string `verbose` both produce a bare `0` through the plain form. Only the `E` variants keep them apart. The README puts it plainly: use `To_____E` when you need to tell the difference between a matched zero value and a conversion that failed.

The name is a pun on the panic that type assertion can cause, and the README leans into it with a Don't Panic line. Nothing here recovers from a bad conversion; it just gives you a return value and, optionally, an error to check.

What the source tree says about the surface area

The top-level files are a readable index of scope: `basic.go`, `number.go`, `string`-and-slice conversions in `slice.go`, `map.go`, `time.go`, `alias.go` for named types, and `indirect.go` for the path where the input is still an `interface{}`. A `generator/` directory sits alongside `zz_generated.go`, which is the naming convention Go tooling uses for generated output. That pairing tells you the alias methods are produced from a definition rather than written by hand, so extending the alias set is a matter of changing an input list and regenerating.

`internal/` holds the shared machinery, and each of the major files has a matching `_test.go` beside it, including an `alias_test.go` and an `indirect_test.go`. Tests exist for the map, number, slice and time paths separately, which is a reasonable signal that the type-specific logic is where the complexity actually lives.

What is not in the tree is as informative as what is. There is no HTTP layer, no reflection-heavy decoder and no dependency beyond the test packages. `go.mod` declares the module as `github.com/spf13/cast` and requires `go 1.21.0`, with `frankban/quicktest` as the only direct requirement and `google/go-cmp` indirect. A library with no runtime dependencies is easy to reason about in a dependency audit.

What it refuses to guess

The README states the policy directly: cast does not attempt to guess what you meant. A string converts to an int only when the string is a representation of an int. Nothing parses loosely formatted input on your behalf.

go
    cast.ToString("mayoneegg")         // "mayonegg"
    cast.ToString(8)                  // "8"
    cast.ToString(8.31)               // "8.31"
    cast.ToString([]byte("one time")) // "one time"
    cast.ToString(nil)                // ""

The numeric examples show the other half of the policy, including the boolean case, which is worth knowing before it surprises you:

go
    cast.ToInt(8)                  // 8
    cast.ToInt(8.31)               // 8
    cast.ToInt("8")                // 8
    cast.ToInt(true)               // 1
    cast.ToInt(false)              // 0

A float truncating to an integer and a boolean mapping to one and zero are both lossy or surprising in isolation, and both are documented in the README rather than left for you to discover. This is the design trade at the centre of the library: it will convert when the conversion is obvious, and it will not invent a conversion when it is not.

Why Hugo's front matter shaped the API

The README states the origin plainly: cast was developed for Hugo, the website engine that reads YAML, TOML or JSON for metadata. That context explains the whole shape of the library. Front matter is untyped data that arrives as `interface{}`, and any template touching it needs a way to say render this as a date, that as a boolean, this slice as a list of strings.

It also explains the decision to always return a value. In a template engine, a missing parameter should render as empty rather than abort the page, so a zero-value fallback is the correct default behaviour for that use case. That same default is a liability in a service where a silently empty configuration field is a bug you want surfaced, which is exactly the case the `E` methods exist for.

The second README argument is about interfaces generally, not just Hugo: if you handle dynamic content through `interface{}`, you need an easy way to convert into a given type. The library positions itself against bare type assertion, claiming to go beyond it while still using assertions internally when possible.

Development tooling: just, devenv and golangci-lint

The project uses just for its task runner, with a `justfile` that defines the full check pipeline. The README notes that devenv and direnv are recommended for an optimal developer experience, which matches the `.envrc`, `devenv.nix`, `devenv.lock` and `devenv.yaml` files at the repository root. That is a Nix-based development environment rather than a hand-rolled setup script.

The three commands the README documents map onto three `justfile` targets:

shell
just test
just lint
just fmt

Behind them, `test` runs `go test -shuffle on -race -v ./...`, so the suite is randomised and race-enabled, and `lint` runs `golangci-lint` with the repository's `.golangci.yaml`, while `fmt` delegates formatting to the same tool. A `check` target combines test and lint. The README says some linter violations can be fixed automatically, which is what `fmt` is for.

If you are only reading this project rather than contributing to it, the tooling still tells you something: the code is small enough that a race-enabled suite and a single linter cover it, and there is no build system beyond the Go toolchain itself.

Release cadence and the gap between tags and commits

The tagged releases are v1.9.1, v1.9.2 and v1.10.0, with v1.10.0 published on 2025-09-08, while the last push to the repository was on 2026-09-09. Both facts are true at once, and the gap between them is worth naming: work has continued on the default branch without a new tag for about a year. The repository is not archived, so nothing here suggests abandonment, but if you pin a version you are pinning to code that predates the most recent commits.

That has a practical consequence for a library this small. A cast library changes when it decides a new conversion should exist or an existing one should change behaviour, and those changes only reach you at a tag. Reading the commit history is the only way to know what sits between v1.10.0 and the current branch.

Licensing is the uncomplicated part. The README and the `LICENSE` file at the repository root both point at the MIT License, and there is no CLA file, no contributor licensing agreement and no dual-licensing arrangement to think about.

Editorial conclusion

Reach for cast when your input arrives as `interface{}` from YAML, TOML, JSON or a plugin boundary and you want one conversion path instead of a switch statement of type assertions. Keep using plain Go conversion when both sides are statically typed, because a library call there buys nothing. Before adding the dependency, check `generator/` and `zz_generated.go` to see whether the alias you want already exists, run `just test` and `just lint` against your own Go version, and remember that `go.mod` still declares `go 1.21.0` even though the last push was on 2026-09-09.

Frequently asked questions

Does Go have type casting, and what does cast add to it?

Go has type assertion and conversion, and cast sits on top of both, using assertion internally when it can. What it adds is a single conversion path for values that arrive as interface{} from YAML, TOML, JSON or similar untyped sources, with a paired E variant that returns an error so a failed conversion can be told apart from a real zero value.

What is the difference between cast.ToString and cast.ToStringE?

Both return a string. ToString returns the zero value, an empty string, when the input cannot convert, while ToStringE returns that empty string along with an error. Use the E form whenever an empty result would otherwise be indistinguishable from a legitimate empty input.

Does spf13/cast convert structs into other structs?

No. The README describes conversions between basic types, numbers, slices, maps, times and named aliases, and the source files match that list, with alias.go and a generator directory producing the named-type methods. Struct-to-struct conversion, where each field is converted individually, is not something the library does, and the related search phrase for it reflects a recurring question the repository does not answer.

Official sources

  1. Issues
  2. License: MIT
  3. README
  4. Releases
  5. spf13/cast on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/spf13-cast.svg)](https://hysenlabs.com/projects/spf13-cast)