Skip to content

Choose a naming policy

When a serializer and a schema generator spell the same property differently, you get a contract mismatch. The experimental naming package gives you shared pure functions and explicit policies. It does not select a product default, configure encoding/json, interpret tags or rename existing stored data.

Choose the policy at your product boundary

Section titled “Choose the policy at your product boundary”
PolicyProperty PersonProperty URLValueModel Person in Cratis.Models
naming.Default{}PersonURLValuePerson
naming.CamelCasePolicy{}personURLValueperson
naming.Namespaced{}PersonURLValuecratis-models-Person

Use Default to preserve spelling, CamelCasePolicy for C# Fundamentals’ acronym-friendly names, or Namespaced for namespaced storage names. The zero values are ready to use. All supplied policies are immutable and concurrency-safe; there are no mutable package-global defaults. CamelCasePolicy has a suffix because CamelCase is already the pure function’s name.

The Policy interface exposes GetPropertyName(name string) string and GetReadModelName(namespace, name string) string. Supply an explicit dot-separated namespace and model name; the package does not infer them from Go types. Each consumer still owns its default, tag precedence and explicit storage-name overrides. If an explicit storage-name override exists, use it unchanged without invoking the policy. Otherwise, pluralize the inferred model name if required before calling the policy. No policy pluralizes names.

package main
import (
"fmt"
"github.com/cratis/fundamentals.go/naming"
)
func main() {
fmt.Println(naming.CamelCase("Person")) // person
fmt.Println(naming.CamelCase("URLValue")) // URLValue
fmt.Println(naming.CamelCase("ABCdef")) // ABCdef
fmt.Println(naming.PascalCase("person_name")) // Person_name
}

Run go run . to print:

person
URLValue
ABCdef
Person_name

CamelCase leaves the whole string unchanged when it is empty, its first UTF-16 character is not uppercase, or its first two characters are uppercase. Otherwise it uses the FixCamelCasing loop copied by the C# extension. PascalCase uppercases the first rune only if it is in the BMP; supplementary runes remain unchanged. Neither splits words, strips underscores, trims spaces or normalizes accents. This is not ordinary System.Text.Json.JsonNamingPolicy.CamelCase, which turns URLValue into urlValue and ID into id.

package main
import (
"fmt"
"github.com/cratis/fundamentals.go/naming"
)
func main() {
policy := naming.NewNamespaced(1, '/', "app:", true)
fmt.Println(policy.GetReadModelName("Cratis.ReadModels.People", "Person"))
// app:readModels/people-person
fmt.Println(policy.GetPropertyName("Person")) // person
}

Run go run . to print:

app:readModels/people-person
person

Both programs and their output are checked against the naming examples.

The constructor takes segments to skip, a namespace-segment separator, a verbatim prefix and whether to camel-case properties and model names. Negative skipping acts like zero; skipping beyond the namespace length retains no segments. Empty segments are retained. Namespace segments always use CamelCase, independently of the model/property flag.

The final separator before the model is always -, even when the configured separator is / or there is no namespace. An empty namespace gives -Person; skipping every segment with prefix app: and camel casing gives app:-person. This follows the C# implementation literally rather than repairing the output. The zero value uses - between segments, skips none, has no prefix and leaves model/property names unchanged. A constructed NUL separator is supported.

Authority is C# Fundamentals at d2accc4a79b6bcf2708213c97093ab5ba6c06381: Strings/StringExtensions.cs and the naming policies under Serialization. The golden fixture provenance records exact sources and expectations, including deliberate Go adaptations.

Classification and casing use BMP-aware helpers to match C# UTF-16 char behavior. Supplementary letters are never uppercase or case-converted: 𐐀name and 𐐨name stay unchanged; A𐐀name becomes a𐐀name. The helpers also preserve Turkish İ when lowercasing and ı when uppercasing, matching .NET’s explicit invariant exceptions. Long s (ſ) is not an exception in that source and uppercases to S. Neither simple mapping expands ß to SS or normalizes combining marks.

Remaining representation differences and omissions:

  • Casing tables depend on the Go toolchain versus the .NET runtime/globalization backend; arbitrary Unicode names are not promised byte-for-byte cross-runtime equivalence.
  • Go has no null string or unpaired UTF-16 character representation. Pascal casing reconstructs runes and replaces invalid UTF-8 bytes with U+FFFD. Camel casing preserves original bytes on early returns, but its converting path replaces invalid bytes throughout the string.
  • A separator is a Go rune, not a C# char: supplementary separators work in Go; invalid runes (including surrogate values) become U+FFFD. C# can retain a surrogate code unit.
  • There is no Humanizer pluralization, CLR Type/ReadModelNameAttribute inspection, JsonNamingPolicy object or IServiceCollection registration. The string-based model API corresponds to C# with pluralization disabled. Pass explicit metadata; use an override unchanged without invoking a policy, or pluralize the inferred name if required before calling the policy.

Prefer explicit wire/storage names for cross-runtime Unicode boundaries or already-persisted data. Adopting this package is not authorization to migrate names. The parity map distinguishes shared behavior from consumer adoption; executable usage examples live in naming/example_test.go.