Skip to content
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

Add json (un)marshalling methods on NullUUID #44

Closed
wants to merge 1 commit into from
Closed

Add json (un)marshalling methods on NullUUID #44

wants to merge 1 commit into from

Conversation

blastrock
Copy link

Without these methods, NullUUID is marshalled as an object with an UUID and a Valid properties. This allows to marshal it as null or a string.

@coveralls
Copy link

coveralls commented Mar 15, 2017

Coverage Status

Coverage decreased (-0.5%) to 96.057% when pulling 6b5babb on blastrock:json_marshalling into b061729 on satori:master.

@satori
Copy link
Owner

satori commented Mar 16, 2017

@blastrock could you please reimplement it with encoding.TextMarshaler and encoding.TextUnmarshaler interfaces?

@blastrock
Copy link
Author

I'm new to go, so maybe I got something wrong, but if I implement MarshalText like this:

func (u NullUUID) MarshalText() ([]byte, error) {
	if u.Valid == false {
		return []byte("null"), nil
	}
	return []byte(u.UUID.String()), nil
}

A null UUID is marshalled in JSON as a string "null" instead of a null value. If I return nil instead of []byte("null"), then it gets marshalled as an empty string "".

@dmitshur
Copy link

dmitshur commented Mar 18, 2017

@blastrock Documentation of json.Marshal at https://godoc.org/encoding/json#Marshal states:

If an encountered value implements the Marshaler interface and is not a nil pointer, Marshal calls its MarshalJSON method to produce JSON. If no MarshalJSON method is present but the value implements encoding.TextMarshaler instead, Marshal calls its MarshalText method and encodes the result as a JSON string.

The second sentence explains the behavior you're seeing.

@blastrock
Copy link
Author

Indeed, but this is not the behavior one would expect, as a null UUID is better represented by a null JSON value than an empty string.

In my use-case at least, I need a null JSON value when a NullUUID is not Valid, and I think only MarshalJSON can provide me with this behavior.

@dmitshur
Copy link

dmitshur commented Mar 18, 2017

Yes, I agree. I don't think you'll be able to use just encoding.TextMarshaler to achieve that, you'll need json.Marshaler.

uuid.go Outdated
u.Valid = false
}
return nil
}
Copy link
Owner

@satori satori Mar 22, 2017

Choose a reason for hiding this comment

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

Would something like that make UnmarshalJSON a bit cleaner?

func (u *NullUUID) UnmarshalJSON(b []byte) error {
    if bytes.Equal(b, []byte("null")) {
        return nil
    }
    if err := json.Unmarshal(b, &u.UUID); err != nil {
        return err
    }
    u.Valid = true
    return nil
}

Copy link

@dmitshur dmitshur Mar 22, 2017

Choose a reason for hiding this comment

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

I highly prefer being explicit about return values, I think it's much more readable to see return err or return nil than just seeing return and trying to infer what's being returned.

According to Go style:

Naked returns are okay if the function is a handful of lines. Once it's a medium sized function, be explicit with your return values.

I'd say this is a medium sized function.

Also:

Corollary: it's not worth it to name result parameters just because it enables you to use naked returns. Clarity of docs is always more important than saving a line or two in your function.

Copy link
Owner

Choose a reason for hiding this comment

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

@shurcooL thanks for your valuable comment, I've edited my code quirk

Copy link
Author

Choose a reason for hiding this comment

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

So, you removed u.Valid = false part. I thought it would be better to explicitly initialize all the fields and not rely on default initialization. I don't mind changing that if you want.

As for the comparison with "null", it injects json logic in the function and that should be the responsibility of the json package. Maybe the contents of the buffer is not "null" but " null " and that comparison would fail, but the json package will parse it correctly.

Choose a reason for hiding this comment

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

So, you removed u.Valid = false part. I thought it would be better to explicitly initialize all the fields and not rely on default initialization. I don't mind changing that if you want.

I think you need to explicitly initialize all fields, you cannot rely on default initialization. The value being unmarshaled into may be zero, or it may not be zero. Removing u.Valid = false would make it fail to operate correctly on &NullUUID{Valid: true} starting value.

Copy link
Owner

Choose a reason for hiding this comment

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

@blastrock JSON decoder tokenizes input and it will pass "null" to UnmarshalJSON in any way.

If you look closer besides stylistic changes I removed intermediate UUID allocation by passing &u.UUID to json.Unmarshal.
Simple benchmark of original UnmarshalJSON and patched one:

BenchmarkUnmarshalJSON-4   	 2000000	       879 ns/op	     312 B/op	       3 allocs/op

vs

BenchmarkUnmarshalJSON-4   	 2000000	       770 ns/op	     288 B/op	       1 allocs/op

Benchmark itself:

func BenchmarkUnmarshalJSON(b *testing.B) {
    bytes := []byte("\"886313e1-3b8a-5372-9b90-0c9aee199e5d\"")
    u := NullUUID{}
    for i := 0; i < b.N; i++ {
        u.UnmarshalJSON(bytes)
    }
}

Just for comparison UnmarshalText benchmark does 0 allocations and 10x faster:

BenchmarkUnmarshalText-4   	20000000	        88.9 ns/op	       0 B/op	       0 allocs/op

I agree with @shurcooL's comment about initializing all fields but it might be Scan method does it the most correct way by initializing UUID struct member to be Nil too.

Copy link
Author

Choose a reason for hiding this comment

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

@blastrock JSON decoder tokenizes input and it will pass "null" to UnmarshalJSON in any way.

Indeed, it seems so. But then u.UnmarshalJson(" null ") wouldn't work, but it may be acceptable. Just tell me your decision on that and I'll update the PR.

If you look closer besides stylistic changes I removed intermediate UUID allocation by passing &u.UUID to json.Unmarshal.

Right, I didn't notice that, and I don't see any other way to remove that allocation.

I agree with @shurcooL's comment about initializing all fields but it might be Scan method does it the most correct way by initializing UUID struct member to be Nil too.

Will do.

@coveralls
Copy link

coveralls commented Mar 30, 2017

Coverage Status

Coverage decreased (-0.5%) to 96.029% when pulling d39caff on blastrock:json_marshalling into 5bf94b6 on satori:master.

uuid.go Outdated
return nil
}

u.Valid = true

Choose a reason for hiding this comment

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

Wouldn't it make more sense to do u.Valid = true after if err := json.Unmarshal(b, &u.UUID); err != nil { ... }? What's the reason you decided to put it on top, instead of below?

Copy link
Author

Choose a reason for hiding this comment

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

You are right, putting it after the if makes more sense.

@coveralls
Copy link

Coverage Status

Coverage decreased (-0.5%) to 96.043% when pulling b99e53e on blastrock:json_marshalling into 5bf94b6 on satori:master.

1 similar comment
@coveralls
Copy link

coveralls commented Mar 31, 2017

Coverage Status

Coverage decreased (-0.5%) to 96.043% when pulling b99e53e on blastrock:json_marshalling into 5bf94b6 on satori:master.

@prettymuchbryce
Copy link

This seems like a good change. Any updates here?

@RangelReale
Copy link

I am having the same problem, I think this PR is good too.

@prettymuchbryce
Copy link

I have created this small package as a workaround for now. I hope this PR can be merged so I can use satori/go.uuid directly.

@jpfluger
Copy link

👍 The latest commits work for me in conjunction with go-pg. NullUUID has json marshalled using the json encoder, as appropriate. Here's the quick links to MarshallJSON and UnmarshallJSON.

@rstrlcpy
Copy link

rstrlcpy commented May 3, 2018

Any updates? When this PR will be merged?

@theckman
Copy link

@blastrock Some of us in the Go community have forked this repository with the intent of maintaining it long-term. It seems like this proposed change could be a good addition to the new fork. Would you be interested in raising this change against our repo?


// UnmarshalJSON unmarshalls a NullUUID
func (u *NullUUID) UnmarshalJSON(b []byte) error {
if bytes.Equal(b, []byte("null")) {

Choose a reason for hiding this comment

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

Should we mark this as an invalid UUID? It seems like sending null could be a very explicit way to do the same thing, without sending 16 NULL bytes.

Copy link
Author

Choose a reason for hiding this comment

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

I am not sure what you mean here, we must fill the NullUUID struct. Unless we change it to contain a pointer, we can't set u.UUID to nil, can we?

@blastrock
Copy link
Author

@theckman thanks for forking and maintaining this package! I'll do that when I have some time. To tell the truth, we are not using this package anymore in our project. Of course, I wouldn't mind if you just take this code and put it in your fork, and I don't care about getting my name in the commit either ^^

theckman added a commit to gofrs/uuid that referenced this pull request Jul 23, 2018
This commit adapts satori/go.uuid#44 for our fork of the
original project. This brings JSON Marshaling and Unmarshaling to the
`uuid.NullUUID` type used for use with SQL databases.

This is needed because `uuid.NullUUID` is a shim around `uuid.UUID`, providing
the information needed by the `database/sql` package to support storing the
value in a nullable column. Without these methods, the type gets converted
unnecessarily to a different format than the standard `uuid.UUID` type.

Signed-off-by: Tim Heckman <t@heckman.io>
@theckman
Copy link

@blastrock I took a swing adapting this PR for our fork, and was able to keep a commit attributing it to you. gofrs/uuid#38

theckman added a commit to gofrs/uuid that referenced this pull request Jul 23, 2018
This commit adapts satori/go.uuid#44 for our fork of the
original project. This brings JSON Marshaling and Unmarshaling to the
`uuid.NullUUID` type used for use with SQL databases.

This is needed because `uuid.NullUUID` is a shim around `uuid.UUID`, providing
the information needed by the `database/sql` package to support storing the
value in a nullable column. Without these methods, the type gets converted
unnecessarily to a different format than the standard `uuid.UUID` type.

Signed-off-by: Tim Heckman <t@heckman.io>
theckman added a commit to gofrs/uuid that referenced this pull request Jul 23, 2018
This commit adapts satori/go.uuid#44 for our fork of the
original project. This brings JSON Marshaling and Unmarshaling to the
`uuid.NullUUID` type used for use with SQL databases.

This is needed because `uuid.NullUUID` is a shim around `uuid.UUID`, providing
the information needed by the `database/sql` package to support storing the
value in a nullable column. Without these methods, the type gets converted
unnecessarily to a different format than the standard `uuid.UUID` type.

Signed-off-by: Tim Heckman <t@heckman.io>
theckman added a commit to gofrs/uuid that referenced this pull request Jul 23, 2018
This commit adapts satori/go.uuid#44 for our fork of the
original project. This brings JSON Marshaling and Unmarshaling to the
`uuid.NullUUID` type used for use with SQL databases.

This is needed because `uuid.NullUUID` is a shim around `uuid.UUID`, providing
the information needed by the `database/sql` package to support storing the
value in a nullable column. Without these methods, the type gets converted
unnecessarily to a different format than the standard `uuid.UUID` type.

Signed-off-by: Tim Heckman <t@heckman.io>
theckman added a commit to gofrs/uuid that referenced this pull request Jul 23, 2018
This commit adapts satori/go.uuid#44 for our fork of the
original project. This brings JSON Marshaling and Unmarshaling to the
`uuid.NullUUID` type used for use with SQL databases.

This is needed because `uuid.NullUUID` is a shim around `uuid.UUID`, providing
the information needed by the `database/sql` package to support storing the
value in a nullable column. Without these methods, the type gets converted
unnecessarily to a different format than the standard `uuid.UUID` type.

Signed-off-by: Tim Heckman <t@heckman.io>
theckman added a commit to gofrs/uuid that referenced this pull request Jul 23, 2018
This commit adapts satori/go.uuid#44 for our fork of the
original project. This brings JSON Marshaling and Unmarshaling to the
`uuid.NullUUID` type used for use with SQL databases.

This is needed because `uuid.NullUUID` is a shim around `uuid.UUID`, providing
the information needed by the `database/sql` package to support storing the
value in a nullable column. Without these methods, the type gets converted
unnecessarily to a different format than the standard `uuid.UUID` type.

Signed-off-by: Tim Heckman <t@heckman.io>
@blastrock
Copy link
Author

Great. I'm a bit late but I just reviewed it, seems good to me :)

Thank you!

@blastrock blastrock closed this Jul 29, 2018
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.

9 participants