To_json key ordering in output?

Apologies if this has been documented elsewhere, I had a look around and couldn’t find anything.

When using JSON::Serializable and to_json to turn objects into JSON, particularly classes and structs, I’ve noticed that the keys of the output are always sorted alphanumerically, rather than according to their order in the original object.

For example this:

struct PagedResponse(U)
    include JSON::Serializable

    property page : Int64
    @count : Int64
    @[JSON::Field(emit_null: true)]
    @previous : String?
    @[JSON::Field(key: "next", emit_null: true)]
    @next_page : String?
    @[JSON::Field(emit_null: true)]
    @results : Array(U)
  end

turns out like this, with alpha-sorted keys:

{
    "count": 95,
    "next": "https://127.0.0.1:8080/attributes/?page=2",
    "page": 1,
    "previous": null,
    "results": [ //omitted ]
}

It’s not a big deal, but for APIs it’s nice to be able to guarantee the order, especially if you end up with a large array like results there having some other keys after it, making it look messier for other humans reading it.

Is there a way I can get around this and guarantee output ordering?

You can define a custom to_json method to fully control the generated JSON. But there’s really little point to it. Field order in JSON objects is technically irrelevant.

Thanks for the quick reply! I wondered if anyone would respond with that :slight_smile:

While you’re technically quite right, I would argue it’s much nicer for humans to consume something like this, where the smaller values are grouped together:

{ 
    "count": 95,
    "next": "https://127.0.0.1:8080/attributes/?page=2",
    "page": 1,
    "previous": null,
    "results": [
       { "thing": 1 },
       { "thing": 2 },
       { "thing": 3 },
       { "thing": 4 },
       { "thing": 5 },
       { "thing": 6 },
       { "thing": 7 },
       { "thing": 8 },
       { "thing": 9 },
       { "thing": 10 }
    ]
}

than this, where they’re split by a big value in the middle:

{
    "count": 95,
    "data": [ 
       { "thing": 1 },
       { "thing": 2 },
       { "thing": 3 },
       { "thing": 4 },
       { "thing": 5 },
       { "thing": 6 },
       { "thing": 7 },
       { "thing": 8 },
       { "thing": 9 },
       { "thing": 10 }
    ],
    "next": "https://127.0.0.1:8080/attributes/?page=2",
    "page": 1,
    "previous": null,
}

(Imagine that there’s a lot more within that array.)

As I said, it’s not a big deal, but people reading documentation and examples of APIs would find the former more pleasant, I suspect. I do, and I’d like to be able to provide as nice an experience for others using my API as I can.

I guess I’ll look into defining my own to_json, assuming it’s possible to access the original fields in their original order. Thanks!

@joshsharp I tried your code, the output is in the order you define the instance vars:

https://play.crystal-lang.org/#/r/9e90

I tried this because I don’t remember I sorted the instance vars in the compiler…

Could you please provide some reproducible code?

Argh, you’re right! I’m sorry, I feel so silly. I started piping it through python -m json.tool for pretty-printed output, but this version sorts the keys by default — I was also looking at the wrong version of the Python docs :upside_down_face: Thanks for your help! I’m glad it was my fault and it’s not even an issue.

FWIW My serializer shard supports controlling the order if you need things in a specific order for whatever reason. See https://athena-framework.github.io/serializer/Athena/Serializer/Annotations/AccessorOrder.html.

1 Like

Athena looks interesting, cheers!

1 Like