Skip to content

Feature request: selective transform execution by tag name (multi-phase transform pipeline) #52

Description

@VasuGajjar

Summary

We've been using mold in production and really appreciate the library — it handles the vast majority of our transform needs cleanly. This is a feature suggestion that would make it even more powerful for multi-step pipelines.

Currently, Transformer.Struct() runs all registered transforms in a single pass. It would be great to have a way to selectively run only certain transforms (by tag name) in a given call — enabling a natural "pre-parse" and "post-parse" transform pattern with a single struct definition.


Background: the multi-phase transform use case

A typical HTTP handler pipeline looks like this:

json.Unmarshal(body, &dto)   // parse
transformer.Struct(&dto)     // transform (trim, lcase, default=X, ...)
validator.Struct(&dto)       // validate

This works well for most transforms (trim, lcase, ucase, etc.) since they operate on values the client explicitly sent. However, it can make default=X tricky for pointer types and for fields where the Go zero-value is a valid client-supplied value.

Why default=X is hard to use correctly after parsing

Consider this struct:

type UpdateRequest struct {
    Source  *string `json:"source"  mold:"default=web"`
    Visible bool    `json:"visible" mold:"default=true"`
    Limit   int     `json:"limit"   mold:"default=10"`
}
Client sends Field after parse After default=X Intended?
key absent nil / false / 0 "web" / true / 10 ✅ correct
"source": "mobile" *"mobile" *"mobile" ✅ correct
"source": null nil "web" ← overwritten ❌ not ideal
"visible": false false true ← overwritten ❌ not ideal
"limit": 0 0 10 ← overwritten ❌ not ideal

The challenge: after parsing, a field explicitly set to null, false, or 0 by the client looks identical to a field that was never sent — both are Go zero values. So default=X has no way to distinguish them and ends up overwriting deliberate client values.

This makes it difficult to correctly support opt-in boolean fields, nullable pointer fields, or zero-as-valid-value fields using default=X in a post-parse transform pass.


A nicer order: default → parse → transform → validate

Running default=X before parsing on a zero-value struct neatly solves this:

transformer.Struct(&dto)     // phase 1: apply "default=X" to zero-value struct
json.Unmarshal(body, &dto)   // parse: client values overwrite defaults for present keys
transformer.Struct(&dto)     // phase 2: trim, lcase, etc. on decoded values
validator.Struct(&dto)       // validate

Fields absent from the JSON payload keep the default (unmarshalling leaves them untouched), while fields explicitly sent (even null, false, 0) have the parser overwrite the default, so the client always wins.

Client sends After phase-1 default After parse After phase-2 trim/lcase Intended?
key absent "web" "web" (untouched) "web"
"source": "mobile" "web" "mobile" "mobile"
"source": null "web" nil nil ✅ client explicitly cleared it
"visible": false true false false ✅ client explicitly set false
"limit": 0 10 0 0 ✅ client explicitly set zero

Suggested API

Any of the following approaches would cover this use case nicely:

Option A — filter execution by tag name (minimal change, most immediately useful)

// Run only transforms whose modifier matches the given names
transformer.StructFiltered(ctx, &dto, "default")

json.Unmarshal(body, &dto)

// Run all transforms except "default"
transformer.StructExcluding(ctx, &dto, "default")

Option B — factory for tag-scoped Transformer instances

// A cleaner alternative to manually maintaining two instances via SetTagName:
preTransformer  := mold.NewForTags("default")
postTransformer := mold.NewExcludingTags("default")

Option C — struct-level tag grouping (most expressive)

type UpdateRequest struct {
    Source *string `json:"source" mold_pre:"default=web" mold_post:"trim"`
}
// mold_pre runs before parse, mold_post runs after parse

We think Option A would be the easiest to adopt and already covers the common case well.


Current workaround

We currently handle this by creating two Transformer instances with different SetTagName values (pretransform and transform) and annotating structs with two separate tags. It works, but it means every struct author needs to be aware of the two-phase pipeline, and transform registrations have to be duplicated. A first-class API for this would be a much cleaner solution.


Environment

  • github.com/go-playground/mold/v4 v4.5.1
    • Go 1.23+

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions