Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

encoding/json: no way to preserve the order of map keys #27179

Open
lavalamp opened this issue Aug 23, 2018 · 28 comments
Open

encoding/json: no way to preserve the order of map keys #27179

lavalamp opened this issue Aug 23, 2018 · 28 comments
Labels
FeatureRequest NeedsDecision Feedback is required from experts, contributors, and/or the community before a change can be made.
Milestone

Comments

@lavalamp
Copy link

The encoding/json library apparently offers no way to decode data into a interface{} without scrambling the order of the keys in objects. Generally this isn't a correctness issue (per the spec the order isn't important), but it is super annoying to humans who may look at JSON data before and after it's been processed by a go program.

The most popular yaml library solves the problem like this: https://godoc.org/gopkg.in/yaml.v2#MapSlice

@ALTree
Copy link
Member

ALTree commented Aug 23, 2018

This was asked in the past. Last week: #27050, and an old issue: #6244, where brad wrote:

JSON objects are unordered, just like Go maps. If you're depending on the order that a specific implementation serializes your JSON objects in, you have a bug.

While it's unlikely that the current behaviour of the json package will change, a proposal to add some new mechanism that preserves order (similar to the one in the yaml package) may be accepted. It would help, though, to have something more concrete to look at, i.e. a complete, full fledged proposal explaining what the new piece of API would look like.

@lavalamp
Copy link
Author

My search failed to turn up either of those issues, thanks.

I tried to make it clear that this is not about correctness, it's about being nice to human readers. If that's not a relevant concern then we can close this. If there is some chance of a change being accepted then I can make a proposal.

@ALTree ALTree added the NeedsDecision Feedback is required from experts, contributors, and/or the community before a change can be made. label Aug 23, 2018
@ALTree
Copy link
Member

ALTree commented Aug 23, 2018

If there is some chance of a change being accepted then I can make a proposal.

I don't know if there is but the package owners can probably tell you: cc @rsc @dsnet @bradfitz

@ALTree ALTree added this to the Go1.12 milestone Aug 23, 2018
@mvdan
Copy link
Member

mvdan commented Aug 23, 2018

There is a bit of precedent to making things nicer for human readers. For example, fmt sorts maps when printing them, to avoid random ordering in the output.

The fmt and json packages are different, and sorting keys isn't the same as keeping the original order, but I think the purpose is similar. Making the output easy to read or modify by humans.

I imagine there's no way to do this under the hood or by simply adding extra API like Encoder.SetIndent, as native maps simply don't keep the order.

Adding a type like MapSlice sounds fine to me - I'd say file a proposal unless someone else comes up with a better idea in the next few days. There's the question of whether the proposal will get past "publish this as a third-party package", but otherwise it seems like a sound idea.

@mvdan
Copy link
Member

mvdan commented Aug 23, 2018

Similar proposal in the past - keeping the order of the headers in net/http: #24375

Just like in this case, the big question was where to store the actual order of the map keys.

@mvdan mvdan changed the title encoding/json: no way to preserve order encoding/json: no way to preserve the order of map keys Aug 23, 2018
@dsnet
Copy link
Member

dsnet commented Aug 23, 2018

Strictly speaking, you can do this today using the raw tokenizer, that json.Decoder provides. That said, I think there will be comprehensive review of all json proposals and issues in the Go1.12 cycle. I can imagine solutions for this issue that also address problems of not being able to detect duplicate keys in objects.

@lavalamp
Copy link
Author

I think the proposal would look like a flag (.SetPreserveOrder()) on the decoder that makes a MapSlice instead of a map[string]interface{}, plus making the encoder handle that type.

@dsnet Yeah, that solves the input half of the problem, but it's very inconvenient.

@dsnet
Copy link
Member

dsnet commented Aug 23, 2018

Alternatively, it could output as [][2]interface{}, in which case you won't need to declare a new type in the json package.

@lavalamp
Copy link
Author

I actually like that a lot. It should probably still be declared in the json package just for documentation purposes.

@rsc
Copy link
Contributor

rsc commented Sep 26, 2018

For now it seems like the best answer is a custom type (maybe a slice of pairs) with an UnmarshalJSON method that in turn uses the tokenizer.

@rsc rsc modified the milestones: Go1.12, Go1.13 Nov 14, 2018
@Zamiell
Copy link

Zamiell commented Apr 20, 2019

For the people who stumble upon this issue from Google, the following two libraries (pick one) can help you if you need an ordered JSON map:

https://gitlab.com/c0b/go-ordered-json
https://github.com/iancoleman/orderedmap

Also, for reference, see this common StackOverflow answer:
https://stackoverflow.com/questions/25182923/serialize-a-map-using-a-specific-order

Of course, it would be fantastic if this were eventually part of the standard library, so I'll eagerly await a proposal from someone more proficient than I.

@roshangade
Copy link

As per JSON specification, order does not matter.
Reference: https://tools.ietf.org/html/rfc7159#section-1

Other technologies, which use JSON, where order matters.
Example: GraphQL
Reference: https://graphql.github.io/graphql-spec/June2018/#sec-Serialized-Map-Ordering

It's a much-needed enhancement.

@astleychen
Copy link

Feature needed to simplify our implementation on ordered serialization/deserialization.

@rsc rsc modified the milestones: Go1.14, Backlog Oct 9, 2019
@eliben
Copy link
Member

eliben commented Nov 27, 2019

Strictly speaking, you can do this today using the raw tokenizer, that json.Decoder provides. That said, I think there will be comprehensive review of all json proposals and issues in the Go1.12 cycle. I can imagine solutions for this issue that also address problems of not being able to detect duplicate keys in objects.

We just ran into a case where the order of fields in a JSON file was important, and this code snippet was helpful. However, when order matters not only in the top-level JSON object but also in deeper nested objects, the need to preserve order complicates the code significantly - instead of "just" unmarshaling the object we have to do a series of piece-meal unmarshals to json.RawMessage so that we can use the unparsed byte stream at the right level.

@polinasok

@ake-persson
Copy link

ake-persson commented Feb 18, 2020

Using MapSlice in JSON.

type MapItem struct {
        Key, Value interface{}
}

type MapSlice []MapItem

func (ms MapSlice) MarshalJSON() ([]byte, error) {
        buf := &bytes.Buffer{}
        buf.Write([]byte{'{'})
        for i, mi := range ms {
                b, err := json.Marshal(&mi.Value)
                if err != nil {
                        return nil, err
                }
                buf.WriteString(fmt.Sprintf("%q:", fmt.Sprintf("%v", mi.Key)))
                buf.Write(b)
                if i < len(ms)-1 {
                        buf.Write([]byte{','})
                }
        }
        buf.Write([]byte{'}'})
        return buf.Bytes(), nil
}

Complete example with unmarshal in Go Playground

As a package mapslice-json.

@dfurtado
Copy link

dfurtado commented Mar 20, 2020

Hi, I have a question about this issue.

I have been looking at the source code a bit and I found that the function func (me mapEncoder) encode(e *encodeState, v reflect.Value, opts encOpts) in the encode.go actually sort the keys by doing:

sort.Slice(sv, func(i, j int) bool { return sv[i].s < sv[j].s })

Here's a bigger snippet:

// Extract and sort the keys.
keys := v.MapKeys()
sv := make([]reflectWithString, len(keys))
for i, v := range keys {
	sv[i].v = v
	if err := sv[i].resolve(); err != nil {
		e.error(fmt.Errorf("json: encoding error for type %q: %q", v.Type().String(), err.Error()))
	}
}
sort.Slice(sv, func(i, j int) bool { return sv[i].s < sv[j].s })

Why this sort is done in first place? Is there any reason or did I miss something?

I was just giving a go and removed line and the map[string]interface{} go serialized correctly.

cc @rsc @dsnet @bradfitz

Thanks!!! =)

@mvdan
Copy link
Member

mvdan commented Mar 20, 2020

@dfurtado json encoding should be deterministic. Ranging over a map has an unspecified order, so there isn't a stable order we can use by default. So the encoder falls back to the equivalent of sort.Strings.

ondrejbudai added a commit to ondrejbudai/osbuild-composer that referenced this issue Mar 25, 2021
A result from manifest v2 contains logs from pipelines. The individual
pipelines are stored as an object, thus they have no order. Well, at least
in Go, because it doesn't guarantee one when parsing maps, see:

golang/go#27179

Unfortunately, this makes the Result.fromV2 method return unpredictable
results because the pipeline results are processed in basically a random
order.

This caused the TestUnmarshalV2Failure test (result_test.go:124) to randomly
fail because it expects the ordering to be stable. I decided to fix this by
ordering the pipeline results by their name. When this fix is applied, the
output from Result.fromV2 is well-defined.

Signed-off-by: Ondřej Budai <ondrej@budai.cz>
msehnout pushed a commit to osbuild/osbuild-composer that referenced this issue Mar 25, 2021
A result from manifest v2 contains logs from pipelines. The individual
pipelines are stored as an object, thus they have no order. Well, at least
in Go, because it doesn't guarantee one when parsing maps, see:

golang/go#27179

Unfortunately, this makes the Result.fromV2 method return unpredictable
results because the pipeline results are processed in basically a random
order.

This caused the TestUnmarshalV2Failure test (result_test.go:124) to randomly
fail because it expects the ordering to be stable. I decided to fix this by
ordering the pipeline results by their name. When this fix is applied, the
output from Result.fromV2 is well-defined.

Signed-off-by: Ondřej Budai <ondrej@budai.cz>
achilleas-k pushed a commit to achilleas-k/osbuild-composer that referenced this issue Apr 26, 2021
A result from manifest v2 contains logs from pipelines. The individual
pipelines are stored as an object, thus they have no order. Well, at least
in Go, because it doesn't guarantee one when parsing maps, see:

golang/go#27179

Unfortunately, this makes the Result.fromV2 method return unpredictable
results because the pipeline results are processed in basically a random
order.

This caused the TestUnmarshalV2Failure test (result_test.go:124) to randomly
fail because it expects the ordering to be stable. I decided to fix this by
ordering the pipeline results by their name. When this fix is applied, the
output from Result.fromV2 is well-defined.

Signed-off-by: Ondřej Budai <ondrej@budai.cz>
achilleas-k pushed a commit to osbuild/osbuild-composer that referenced this issue Apr 26, 2021
A result from manifest v2 contains logs from pipelines. The individual
pipelines are stored as an object, thus they have no order. Well, at least
in Go, because it doesn't guarantee one when parsing maps, see:

golang/go#27179

Unfortunately, this makes the Result.fromV2 method return unpredictable
results because the pipeline results are processed in basically a random
order.

This caused the TestUnmarshalV2Failure test (result_test.go:124) to randomly
fail because it expects the ordering to be stable. I decided to fix this by
ordering the pipeline results by their name. When this fix is applied, the
output from Result.fromV2 is well-defined.

Signed-off-by: Ondřej Budai <ondrej@budai.cz>
@ryanc414
Copy link

We ran into this issue, where a third-party API we use requires us to extract an object from a JSON response and return it completely unchanged in a subsequent request body, including not changing the key ordering. Our workaround was to treat the object as an opaque byte slice like:

type rawJsonObject []byte

func (o rawJsonObject) MarshalJSON() ([]byte, error) {
	return []byte(o), nil
}

func (o *rawJsonObject) UnmarshalJSON(data []byte) error {
	*o = data
	return nil
}

Not sure if there is any better way to achieve the same thing. Is there any reason why the JSON marshaller cannot do this for []byte types automatically without the need for a custom type and marshal/unmarshal functions?

@mvdan
Copy link
Member

mvdan commented May 26, 2021

@ryanc414 see https://golang.org/pkg/encoding/json/#RawMessage.

@xpol

This comment was marked as duplicate.

@kkqy

This comment was marked as off-topic.

@golang golang locked as resolved and limited conversation to collaborators Mar 24, 2023
@golang golang unlocked this conversation Mar 24, 2023
@wk8

This comment was marked as off-topic.

@7sDream
Copy link

7sDream commented Aug 18, 2023

I recently encountered this issue, after investigating the existing solution/project, unfortunately I found that none of them fully met my three needs: preserve order, allow duplicated key, lossless number.

So I wrote my own solution, It looks like:

data := `{"a": 1.1234543234343238726283746, "b": false, "c": null, "b": "I'm back"}`
result, _ := geko.JSONUnmarshal([]byte(data), geko.UseNumber(true))
output, _ := json.Marshal(result)
fmt.Printf("Output: %s\n", string(output))
// Output: {"a":1.1234543234343238726283746,"b":false,"c":null,"b":"I'm back"}

object := result.(geko.ObjectItems)
fmt.Printf("b: %#v\n", object.Get("b"))
// b: []interface {}{false, "I'm back"}

In the case of someone happens to have the same scenario: geko and it's document.

@yuki2006

This comment was marked as duplicate.

@imReker
Copy link

imReker commented Mar 18, 2024

Go team of Google: You should NOT rely on order of JSON Object, it's written in RFC.
Firebase team of Google: You SHOULD rely on order of JSON Object.

Reference: https://firebase.google.com/docs/reference/remote-config/rest/v1/RemoteConfig#RemoteConfigParameterValue

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
FeatureRequest NeedsDecision Feedback is required from experts, contributors, and/or the community before a change can be made.
Projects
None yet
Development

No branches or pull requests