ActiveModel (by 4.1.1) doesn't have a way to track "inline" modifications on attributes.
Your 'similar_but_different_json' method is probably making inline modifications on the string.
Just duplicate the string before modifying it.
test = Submission.find(1)
test_json_data_duplicate = test.json_data.dup
test.update_attribute('json_data',similar_but_different_json(test_json_data_duplicate))
When you did ...
test.json_data = ""
... ActiveModel could catch the change because you are setting it to a new String object that happens to be empty. So when you call update_attribute the model has already known that the attribute has changed.
If you try to empty the string in an inline manner your trick will not work.
test = Submission.find(1)
old_json_data = test.json_data
test.json_data.clear # Instead of test.json_data = ""
test.json_data = similar_but_different_json(old_json_data)
test.save
ActiveModel::Dirty