Skip to content

Commit

Permalink
Add support for legacy string tag option behavior (#63)
Browse files Browse the repository at this point in the history
In v1, the `string` tag option would stringify bools and strings.
Add an option to support this behavior in v2.
  • Loading branch information
dsnet authored Nov 24, 2024
1 parent b5f849c commit 7696f99
Show file tree
Hide file tree
Showing 4 changed files with 378 additions and 7 deletions.
76 changes: 69 additions & 7 deletions arshal_default.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ func makeBoolArshaler(t reflect.Type) *arshaler {
}

// Optimize for marshaling without preceding whitespace.
if optimizeCommon && !xe.Flags.Get(jsonflags.AnyWhitespace) && !xe.Tokens.Last.NeedObjectName() {
if optimizeCommon && !xe.Flags.Get(jsonflags.AnyWhitespace) && !mo.Flags.Get(jsonflags.StringifyBoolsAndStrings) && !xe.Tokens.Last.NeedObjectName() {
xe.Buf = strconv.AppendBool(xe.Tokens.MayAppendDelim(xe.Buf, 't'), va.Bool())
xe.Tokens.Last.Increment()
if xe.NeedFlush() {
Expand All @@ -135,6 +135,13 @@ func makeBoolArshaler(t reflect.Type) *arshaler {
return nil
}

if mo.Flags.Get(jsonflags.StringifyBoolsAndStrings) {
if va.Bool() {
return enc.WriteToken(jsontext.String("true"))
} else {
return enc.WriteToken(jsontext.String("false"))
}
}
return enc.WriteToken(jsontext.Bool(va.Bool()))
}
fncs.unmarshal = func(dec *jsontext.Decoder, va addressableValue, uo *jsonopts.Struct) error {
Expand All @@ -152,8 +159,22 @@ func makeBoolArshaler(t reflect.Type) *arshaler {
va.SetBool(false)
return nil
case 't', 'f':
va.SetBool(tok.Bool())
return nil
if !uo.Flags.Get(jsonflags.StringifyBoolsAndStrings) {
va.SetBool(tok.Bool())
return nil
}
case '"':
if uo.Flags.Get(jsonflags.StringifyBoolsAndStrings) {
switch tok.String() {
case "true":
va.SetBool(true)
case "false":
va.SetBool(false)
default:
return &SemanticError{action: "unmarshal", JSONKind: k, GoType: t, Err: fmt.Errorf("cannot parse %q as bool", tok.String())}
}
return nil
}
}
return &SemanticError{action: "unmarshal", JSONKind: k, GoType: t}
}
Expand All @@ -170,7 +191,7 @@ func makeStringArshaler(t reflect.Type) *arshaler {

// Optimize for marshaling without preceding whitespace or string escaping.
s := va.String()
if optimizeCommon && !xe.Flags.Get(jsonflags.AnyWhitespace) && !xe.Tokens.Last.NeedObjectName() && !jsonwire.NeedEscape(s) {
if optimizeCommon && !xe.Flags.Get(jsonflags.AnyWhitespace) && !mo.Flags.Get(jsonflags.StringifyBoolsAndStrings) && !xe.Tokens.Last.NeedObjectName() && !jsonwire.NeedEscape(s) {
b := xe.Buf
b = xe.Tokens.MayAppendDelim(b, '"')
b = append(b, '"')
Expand All @@ -184,6 +205,14 @@ func makeStringArshaler(t reflect.Type) *arshaler {
return nil
}

if mo.Flags.Get(jsonflags.StringifyBoolsAndStrings) {
b, err := jsontext.AppendQuote(nil, s) // only fails for invalid UTF-8
q, _ := jsontext.AppendQuote(nil, b) // cannot fail since b is valid UTF-8
if err != nil && !xe.Flags.Get(jsonflags.AllowInvalidUTF8) {
return err
}
return enc.WriteValue(q)
}
return enc.WriteToken(jsontext.String(s))
}
fncs.unmarshal = func(dec *jsontext.Decoder, va addressableValue, uo *jsonopts.Struct) error {
Expand All @@ -203,6 +232,12 @@ func makeStringArshaler(t reflect.Type) *arshaler {
return nil
case '"':
val = jsonwire.UnquoteMayCopy(val, flags.IsVerbatim())
if uo.Flags.Get(jsonflags.StringifyBoolsAndStrings) {
val, err = jsontext.AppendUnquote(nil, val)
if err != nil {
return &SemanticError{action: "unmarshal", JSONKind: '"', GoType: va.Type(), Err: err}
}
}
if xd.StringCache == nil {
xd.StringCache = new(stringCache)
}
Expand Down Expand Up @@ -1015,7 +1050,11 @@ func makeStructArshaler(t reflect.Type) *arshaler {
// Write the object member value.
flagsOriginal := mo.Flags
if f.string {
mo.Flags.Set(jsonflags.StringifyNumbers | 1)
if !mo.Flags.Get(jsonflags.StringifyWithLegacySemantics) {
mo.Flags.Set(jsonflags.StringifyNumbers | 1)
} else if canLegacyStringify(f.typ) {
mo.Flags.Set(jsonflags.StringifyNumbers | jsonflags.StringifyBoolsAndStrings | 1)
}
}
if f.format != "" {
mo.FormatDepth = xe.Tokens.Depth()
Expand Down Expand Up @@ -1154,7 +1193,11 @@ func makeStructArshaler(t reflect.Type) *arshaler {
}
flagsOriginal := uo.Flags
if f.string {
uo.Flags.Set(jsonflags.StringifyNumbers | 1)
if !uo.Flags.Get(jsonflags.StringifyWithLegacySemantics) {
uo.Flags.Set(jsonflags.StringifyNumbers | 1)
} else if canLegacyStringify(f.typ) {
uo.Flags.Set(jsonflags.StringifyNumbers | jsonflags.StringifyBoolsAndStrings | 1)
}
}
if f.format != "" {
uo.FormatDepth = xd.Tokens.Depth()
Expand Down Expand Up @@ -1225,6 +1268,25 @@ func isLegacyEmpty(v addressableValue) bool {
return false
}

// canLegacyStringify reports whether t can be stringified according to v1,
// where t is a bool, string, or number (or unnamed pointer to such).
// In v1, the `string` option does not apply recursively to nested types within
// a composite Go type (e.g., an array, slice, struct, map, or interface).
func canLegacyStringify(t reflect.Type) bool {
// Based on encoding/json.typeFields#[email protected]
if t.Name() == "" && t.Kind() == reflect.Ptr {
t = t.Elem()
}
switch t.Kind() {
case reflect.Bool, reflect.String,
reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr,
reflect.Float32, reflect.Float64:
return true
}
return false
}

func makeSliceArshaler(t reflect.Type) *arshaler {
var fncs arshaler
var (
Expand Down Expand Up @@ -1528,7 +1590,7 @@ func makeInterfaceArshaler(t reflect.Type) *arshaler {
}
// Optimize for the any type if there are no special options.
if optimizeCommon &&
t == anyType && !mo.Flags.Get(jsonflags.StringifyNumbers) && mo.Format == "" &&
t == anyType && !mo.Flags.Get(jsonflags.StringifyNumbers) && !mo.Flags.Get(jsonflags.StringifyBoolsAndStrings) && mo.Format == "" &&
(mo.Marshalers == nil || !mo.Marshalers.(*Marshalers).fromAny) {
return marshalValueAny(enc, va.Elem().Interface(), mo)
}
Expand Down
Loading

0 comments on commit 7696f99

Please sign in to comment.