@@ -54,10 +54,76 @@ def register_renderers
5454              # Renderer proc is evaluated in the controller context. 
5555              headers [ 'Content-Type' ]  =  Mime [ :jsonapi ] . to_s 
5656
57-               ActiveSupport ::Notifications . instrument ( 'render.jsonapi-rails' , 
58-                                                       resources : resources , 
59-                                                       options : options )  do 
60-                 JSON . generate ( renderer . render ( resources ,  options ,  self ) ) 
57+               ActiveSupport ::Notifications . instrument ( 
58+                 'render.jsonapi-rails' , 
59+                 resources : resources , 
60+                 options : options 
61+               )  do 
62+                 # Depending on whether or not a valid cache object is present 
63+                 # in the options, the #render call below will return two 
64+                 # slightly different kinds of hash. 
65+                 # 
66+                 # Both hashes have broadly the following structure, where r is 
67+                 # some representation of a JSON::API resource: 
68+                 # 
69+                 # { 
70+                 #   data: [ r1, r2, r3 ], 
71+                 #   meta: { count: 12345 }, 
72+                 #   jsonapi: { version: "1.0" } 
73+                 # } 
74+                 # 
75+                 # For non-cached calls to this method, the `data` field in the 
76+                 # return value will contain an array of Ruby hashes. 
77+                 # 
78+                 # For cached calls, the `data` field will contain an array of 
79+                 # JSON strings corresponding to the same data. This happens 
80+                 # because jsonapi-renderer caches both the JSON serialization 
81+                 # step as well as the assembly of the relevant attributes into 
82+                 # a JSON::API-compliant structure. Those JSON strings are 
83+                 # created via calls to `to_json`. They are then wrapped in 
84+                 # CachedResourcesProcessor::JSONString. This defines a 
85+                 # `to_json` method which simply returns self, ie - it attempts 
86+                 # to ensure that any further `to_json` calls result in no 
87+                 # changes. 
88+                 # 
89+                 # That isn't what happens in a Rails context, however. Below, 
90+                 # the last step is to convert the entire output hash of the 
91+                 # renderer into a JSON string to send to the client. If we 
92+                 # call `to_json` on the cached output, the already-made JSON 
93+                 # strings in the `data` field will be converted again, 
94+                 # resulting in malformed data reaching the client. This happens 
95+                 # because the ActiveSupport `to_json` takes precedent, meaning 
96+                 # the "no-op" `to_json` definition on JSONString never gets 
97+                 # executed. 
98+                 # 
99+                 # We can get around this by using JSON.generate instead, which 
100+                 # will use the `to_json` defined on JSONString rather than the 
101+                 # ActiveSupport one. 
102+                 # 
103+                 # However, we can't use JSON.generate on the non-cached output. 
104+                 # Doing so means that its `data` field contents are converted 
105+                 # with a non-ActiveSupport `to_json`. This means cached and 
106+                 # non-cached responses have subtle differences in how their 
107+                 # resources are serialized. For example: 
108+                 # 
109+                 # x = Time.new(2021,1,1) 
110+                 # 
111+                 # x.to_json 
112+                 # => "\"2021-01-01T00:00:00.000+00:00\"" 
113+                 # 
114+                 # JSON.generate x 
115+                 # => "\"2021-01-01 00:00:00 +0000\"" 
116+                 # 
117+                 # The different outputs mean we need to take different 
118+                 # approaches when converting the entire payload into JSON, 
119+                 # hence the check below. 
120+                 jsonapi_hash  =  renderer . render ( resources ,  options ,  self ) 
121+ 
122+                 if  jsonapi_hash [ :data ] &.first &.class  == JSONAPI ::Renderer ::CachedResourcesProcessor ::JSONString 
123+                   JSON . generate  jsonapi_hash 
124+                 else 
125+                   jsonapi_hash . to_json 
126+                 end 
61127              end 
62128            end 
63129          end 
0 commit comments