Feature Request: Configurable Float Formatting
Problem Statement
easyjson currently hardcodes the 'g' format specifier in strconv.AppendFloat() calls for all floating-point serialization. This causes issues for financial applications and other use cases where decimal notation is required instead of scientific notation.
Current Implementation:
- File: jwriter/writer.go
- Lines:
- https://github.com/mailru/easyjson/blob/master/jwriter/writer.go#L244
- https://github.com/mailru/easyjson/blob/master/jwriter/writer.go#L254
- https://github.com/mailru/easyjson/blob/master/jwriter/writer.go#L264
- https://github.com/mailru/easyjson/blob/master/jwriter/writer.go#L274
- Code: strconv.AppendFloat(w.Buffer.Buf, float64(n), 'g', -1, 32)
The hardcoded 'g' format can produce scientific notation (e.g., 1.5e-6 instead of 0.000001), which breaks compatibility with systems expecting decimal notation.
Existing Attempts
- PR #356 (alpacahq): Proposed hardcoding 'f' format - breaks backward compatibility
- PR #367 (rhnvrm/zerodha): Configurable flag approach - maintains backward compatibility
Proposed Solutions
Option 1: CLI Flag (zerodha fork)
Add -float_format CLI flag that:
- Defaults to 'g' (maintains backward compatibility)
- Allows users to specify 'f' or other strconv formats
- Implementation similar to the zerodha fork
Based on the zerodha fork analysis, the changes would be:
- jwriter/writer.go: Add FloatFmt string field to Writer struct and floatFmt() helper method
- easyjson/main.go: Add CLI flag parsing and pass format through generator options
- Generator: Update code generation to use the configurable format
Option 2: Struct Tag
Add floatfmt struct tag for field-level control:
type Price struct {
Value float64 `json:"value,floatfmt=f"`
}
(Needs feasibility check)
Option 3: Runtime Configuration
Package-level variable for default format that can be overridden via some generated method.
(Needs feasibility check)
Use Cases
- Financial applications: Require decimal notation for monetary values
- Scientific computing: Need specific precision control
- API compatibility: Must match existing JSON parsing expectations
- Data interchange: Systems expecting non-scientific notation
This is a common pain point for easyjson users, as evidenced by multiple forks and PRs attempting to address it. A configurable solution would:
- Solve the scientific notation issue
- Maintain backward compatibility
- Provide flexibility for different use cases
- Reduce the need for custom forks
I'm willing to contribute the implementation if there's consensus on the approach.
The CLI flag method seems most appropriate as it follows easyjson's existing pattern of configuration options and doesn't break existing code but it hasn't moved ahead and we have been using this. But I am also open to different approaches as well as long as we can fix this.
Field-Level Float Formatting via Struct Tags
I looked into whether we could use struct tags like json:"value,noexponent" to control float formatting per-field, instead of relying on global CLI flags.
Good news: the architecture already supports this pattern.
How I know it's feasible
The codebase already does per-field customization with the asString tag. Looking at gen/encoder.go:52-91, there's a fieldTags struct that holds tag options, and those flow through to select different Writer methods. For example, json:"field,string" calls Float64Str() instead of Float64().
The key insight is that generated code always calls Writer methods. Different tags just select different methods:
- No tag →
out.Float64() - With
string→out.Float64Str()
We can follow the same pattern for noexponent.
Implementation approach
This is similar to what google/go-querystring#24 did for query parameters.
Add the tag option:
// gen/encoder.go
type fieldTags struct {
// existing fields...
noExponent bool
}
// Parse it
case s == "noexponent":
ret.noExponent = true
Add Writer methods for 'f' format:
// jwriter/writer.go
func (w *Writer) Float64NoExp(n float64) {
w.Buffer.EnsureSpace(20)
w.Buffer.Buf = strconv.AppendFloat(w.Buffer.Buf, n, 'f', -1, 64)
}
func (w *Writer) Float64StrNoExp(n float64) {
w.Buffer.EnsureSpace(22)
w.Buffer.Buf = append(w.Buffer.Buf, '"')
w.Buffer.Buf = strconv.AppendFloat(w.Buffer.Buf, n, 'f', -1, 64)
w.Buffer.Buf = append(w.Buffer.Buf, '"')
}
// Same for Float32
Create encoder maps:
// gen/encoder.go
var primitiveNoExpEncoders = map[reflect.Kind]string{
reflect.Float64: "out.Float64NoExp(float64(%v))",
}
var primitiveStringNoExpEncoders = map[reflect.Kind]string{
reflect.Float64: "out.Float64StrNoExp(float64(%v))",
}
Update the type encoder to check tags:
// gen/encoder.go genTypeEncoderNoCheck()
if tags.asString && tags.noExponent {
// use primitiveStringNoExpEncoders
} else if tags.asString {
// use primitiveStringEncoders
} else if tags.noExponent {
// use primitiveNoExpEncoders
}
// default: primitiveEncoders
What this enables
type PriceData struct {
Scientific float64 `json:"sci"` // 1e8
Price float64 `json:"price,noexponent"` // 100000000
Quoted float64 `json:"str,string,noexponent"` // "100000000"
}
This is more flexible than a global flag since you can mix formats in the same struct. It's also composable with other tags like omitempty.
I'm happy to work on a PR if this approach makes sense to you.