Skip to content

Conversation

@thekie
Copy link
Contributor

@thekie thekie commented Nov 17, 2016

… destructively and non-destructively.

I added two new functions merge(other: JSON) and merged(other: JSON) -> JSON. Merging a JSON into another JSON adds all non existing values to the original JSON which are only present in the other JSON.

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:

  • 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.

mergemutates the original JSON, whereas merged works 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.

Copy link
Contributor

@ldiqual ldiqual left a 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!

self = other
}
} else {
self = other
Copy link
Contributor

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.

Copy link
Contributor Author

@thekie thekie Nov 22, 2016

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.

Copy link
Contributor

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?

Copy link
Contributor Author

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.

Copy link
Contributor Author

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?

Copy link
Contributor

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?

Copy link
Contributor Author

@thekie thekie Dec 2, 2016

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!


- parameter other: The JSON which gets merged into this JSON
*/
public mutating func merge(_ other: JSON) {
Copy link
Contributor

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?

Copy link
Contributor Author

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.

- parameter other: The JSON which gets merged into this JSON
- returns: New merged JSON
*/
public func merged(other: JSON) -> JSON {
Copy link
Contributor

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

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Totally!

Copy link
Contributor

@ldiqual ldiqual left a 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!

}
} else {
if typecheck {
print("Couldn't merge, because the JSONs differ in type on top level.")
Copy link
Contributor

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

Copy link
Contributor Author

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.
Copy link
Contributor

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.
Copy link
Contributor

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)
// [
Copy link
Contributor

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?

Copy link
Contributor Author

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. :)

@ldiqual
Copy link
Contributor

ldiqual commented Dec 3, 2016

@danielkiedrowski Awesome! Thank you so much for contributing to SwiftyJSON, this is a great feature. Merging now.

@ldiqual ldiqual merged commit 25cd885 into SwiftyJSON:master Dec 3, 2016
@thekie
Copy link
Contributor Author

thekie commented Dec 7, 2016

Thank you @ldiqual! It was great working with you.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants