-
Notifications
You must be signed in to change notification settings - Fork 3.5k
Added a function to deep merge a JSON object into another JSON object… #725
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
Conversation
… destructively and non-destructively.
ldiqual
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@danielkiedrowski I think it's a pretty easy change to add to the library and it fits the scope of its responsibilities. The code is easy to read and besides my couple comments below it looks close to final. Would you mind also updating README.md and CHANGELOG.md to reflect your changes? Thanks a lot for your contribution!
Source/SwiftyJSON.swift
Outdated
| self = other | ||
| } | ||
| } else { | ||
| self = other |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This strikes me as dangerous, because if you have a.merge(b) with a being a dict and b being an array, you're basically replacing a with b with no error or warning. I would suggest making the function throw an appropriate error indicating the type mismatching.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wouldn't necessary treat that as an error, since the main identifier in a JSON is the key, but I see how this could lead to bugs and confusion.
But on the other hand some applications doesn't want to have their JSONs strictly typed and what to change the types to change dynamically.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you clarify what you mean by
I wouldn't necessary treat that as an error, since the main identifier in a JSON is the
key
I think SwiftyJSON's goal is to be as safe as possible, which is why there is no force unwrap, that all getters are optional and that non-optional *Value have fallback values. My point is that we should always make sure that errors are propagated back to the client and that we corrupt data without one of those errors.
If someone did want to replace the entire JSON when merging with conflicting types, they could simply do:
let json: JSON
do {
json = try json.merged(with: otherJSON)
} catch {
json = otherJSON
}
How does that sound?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My point is that we should always make sure that errors are propagated back to the client and that we corrupt data without one of those errors.
In my opinion it is not an error, but a proper use case of this merging function. And by prohibiting merging two key-value-pairs that are of different types, we would rob users of this possibility.
I love the idea of python, which is "we're all consenting adults here". Programmers mostly know what they are doing, and if there is overriding is happening because of type mismatch it is either on purpose or it is not SwiftyJSON's tasks to prohibit that by cutting functionality.
If someone did want to replace the entire JSON when merging with conflicting types, they could simply do:
This is not the same as what is happening in my implementation. Of course if you call mergeon top level with two JSONs differing in type, you would overwrite the first JSON, but in every other case, just the nested fields in the JSONs are overwritten.
For instance if you have an "id" field with an integer and for whatever reason merge a JSON with an "id" field which holds a string, just this field gets overwritten.
Can you clarify what you mean by
The main point being: In my opinion it is the desired and expected behavior of merging two JSONs, because JSONs are not type-safe.
But I have asked around here in the office, and got one camp being the same opinion as mine and another camp who is with you. :)
Here is my suggestion:
/**
Merges another JSON into this JSON, whereas primitive values which are not present in this JSON are getting added,
present values getting overwritten, array values getting appended and nested JSONs getting merged the same way.
- parameter other: The JSON which gets merged into this JSON
- parameter strict: Enforce typesafe merging. Not matching types for one key will trigger an exception.
*/
public mutating func merge(with other: JSON, strict: Bool = false) throws {
if self.type == other.type {
switch self.type {
case .dictionary:
for (key, _) in other {
try self[key].merge(with: other[key], strict: strict)
}
case .array:
self = JSON(self.arrayValue + other.arrayValue)
default:
self = other
}
} else {
if strict {
throw JSONError.mergeDifferentTypes
}
self = other
}
}Thank you for your time.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hello @ldiqual. Any thoughts on that?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@danielkiedrowski Sorry for the delay on this.
Oh got it, I didn't realize it would replace nested types too. I actually think nested fields make a lot more sense to be replaced when they have different types.
I would expect this:
let a = ["foo": "bar"]
let b = ["foo": 1]
let c = a.merged(with: b) // c.foo == 1
but not this:
let a = ["foo": "bar"]
let b = [1]
let c = a.merged(with: b) // c == [1]
So what I would propose here is to only replace nested fields, but add a sanity type check for the root object. Would that work?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added the sanity type check @ldiqual . 👍 Thank you!
Source/SwiftyJSON.swift
Outdated
|
|
||
| - parameter other: The JSON which gets merged into this JSON | ||
| */ | ||
| public mutating func merge(_ other: JSON) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can't think of a usecase where you'd like to mutate a JSON value instead of returning another value. Do you have an example in mind?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I actually don't know of a specific example, but I guess, if the user has a very big JSON it is a good idea to have this method for performance reasons.
Source/SwiftyJSON.swift
Outdated
| - parameter other: The JSON which gets merged into this JSON | ||
| - returns: New merged JSON | ||
| */ | ||
| public func merged(other: JSON) -> JSON { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
merged(with: JSON) reads a bit better I think
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Totally!
… JSON in case that the JSON differ in type on the top level.
ldiqual
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@danielkiedrowski Thanks for addressing the type issue. A couple other comments, we're almost done!
Source/SwiftyJSON.swift
Outdated
| } | ||
| } else { | ||
| if typecheck { | ||
| print("Couldn't merge, because the JSONs differ in type on top level.") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You should probably throw an error instead of printing
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Totally right! I'll fix that.
README.md
Outdated
|
|
||
| In case, where two fields in a JSON have a different types, the value will get always overwritten. | ||
|
|
||
| There are two different fashions for merging: `merge`modifies the original JSON, whereas `merged` works non-destructively on a copy. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing space after merge
README.md
Outdated
| - In case of both values being a `JSON.Type.array` the values form the array found in the `other` JSON getting appended to the original JSON's array value. | ||
| - In case of both values being a `JSON.Type.dictionary` both JSON-values are getting merged the same way the encapsulating JSON is merged. | ||
|
|
||
| In case, where two fields in a JSON have a different types, the value will get always overwritten. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Extra space after get
| ] | ||
|
|
||
| let updated = original.merge(with: update) | ||
| // [ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The indentation is kind of weird, can you replace all this with 4 spaces?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure about this issue, but i replaced some tabs wit spaces. :)
…erge JSONs differing in type on top level.
|
@danielkiedrowski Awesome! Thank you so much for contributing to SwiftyJSON, this is a great feature. Merging now. |
|
Thank you @ldiqual! It was great working with you. |
… destructively and non-destructively.
I added two new functions
merge(other: JSON)andmerged(other: JSON) -> JSON. Merging a JSON into another JSON adds all non existing values to the original JSON which are only present in theotherJSON.If both JSONs contain a value for the same key, mostly this value gets overwritten in the original JSON, but there are two cases where it provides some special treatment:
JSON.Type.arraythe values form the array found in theotherJSON getting appended to the original JSON's array value.JSON.Type.dictionaryboth JSON-values are getting merged the same way the encapsulating JSON is merged.mergemutates the original JSON, whereasmergedworks non-destructively on a copy.I found this to be an issue due to a stackoverflow.com post and posted my answer there, but also think that this functions might be a good addition to the SwiftJSON project in general.