wI2L/jsondiff

Is it possible to customize the behavior as in json.Marshal/Unmarshal?

Fyria30 opened this issue · 5 comments

Hi! Thank you for the package!

UseCase:
I have nested structures and would like to ignore some fields on low level structures. Now to ignore some fields we need to write rules on top level every time. Like this:


type List[T any] struct {
	Count int
	Items []T
}
type Role struct {
	Name        string
	Description string
}
type Client struct {
	Roles   List[Role]
	Address string
}


func TestPatch(t *testing.T) {
        var old, new Client
	new.Roles.Items = append(new.Roles.Items, Role{Name: "test2", Description: "test2"})
	new.Roles.Items = append(new.Roles.Items, Role{Name: "test", Description: "test"})
	new.Roles.Count = 2

	old.Roles.Items = append(old.Roles.Items, Role{Name: "test", Description: "test"})
	old.Roles.Count = 1

	patch, err := jsondiff.Compare(old, new, jsondiff.Equivalent() , jsondiff.Ignores("/Roles/Count"))
	require.Nil(t, err)`
}

Would be cool to have something like this:

func (instance *List[T]) Compare(source interface{}, target interface{}, opts ...jsondiff.Option) (jsondiff.Patch, error) {
	return jsondiff.Compare(source, target, jsondiff.Ignores("/Count"))
}


and then to get the same result as in first version by:

patch, err := jsondiff.Compare(old, new, jsondiff.Equivalent())
	require.Nil(t, err)`

wI2L commented

Hello,

If I understand correctly, what you are interested in is a jsondiff.Ignores option that would support JSON pointers with wildcard support, such as (in your example): /Roles/*/Count, to ignore the Count field in every object of the Roles array ?

Regarding the solution you describe, I see how this could eventually work, but I don't think that's the most elegant or obvious approach to your use case.

It also cool, but solves the problem only partly.

Let's assume we have the following structures and need to get diff. But List.Count should not be included in path because it depends on len of Items. And we do not need it, so we need to ignore it.


type List[T any] struct {
	Count int
	Items []T
}
type Role struct {
	Name        string
	Description string
}
type Friend struct {
	Name        string
	Description string
}
type Client struct {
	Roles   List[Role]
	Address List[Friend]
}

func main (){

 //...initialization now we have var a,b Client by somehow fullfilled so we need to find diff in roles and friends
 
 patch, err := jsondiff.Compare(old, new, jsondiff.Equivalent() , jsondiff.Ignores("/Roles/Count"), jsondiff.Ignores("/Friends/Count"))
	require.Nil(t, err)`
}

So we need manually write jsondiff.Ignores("/Roles/Count") , jsondiff.Ignores("/Friends/Count") every time instead to do it once in Compare Customize, and do it for every field with List type. Then we can imagine what we need to do for


type Event struct{
 Client List[Clients]
 Items List[Items]
} 
 

 patch, err := jsondiff.Compare(old, new, jsondiff.Equivalent() , jsondiff.Ignores("Clients/Roles/Count"), jsondiff.Ignores("Clients/Friends/Count"),jsondiff.Ignores("Items/.../Count") )
	require.Nil(t, err)`
}

But would be cool to exclude it once in List method.

func (instance *List[T]) Compare(source interface{}, target interface{}, opts ...jsondiff.Option) (jsondiff.Patch, error) {
	return jsondiff.Compare(source, target, jsondiff.Ignores("/Count"))
}

and then this give us count ignore in all List fields

 patch, err := jsondiff.Compare(old, new, jsondiff.Equivalent()) )
	require.Nil(t, err)`
}

Probably you can advise better solution for the case

wI2L commented

Yep, I get the idea now, but unfortunately the package does not work directly with concrete types, instead it first marshal the input objects to JSON and unmarshal them again in an interface{} to get a simpler representation based only on native Go types.

As such, there is no way to invoke a method like the example you gave (*List[T].Compare) during the comparison, since it won't process a *List[T] type but instead a map[string]interface{}, and then a []interface{} instead of *List[T].Items.

Thank you for reply!

Alright, i got it.

I may have another solution for your use case using a custom implementation of the MarshalJSON method for your custom type List[T].

Would be a way, but i need classic marhsal/unmarshal as well and can't change the implementation of this.

Anyway thank you for the replay and for your work on this project!

wI2L commented

Would be a way, but i need classic marhsal/unmarshal as well and can't change the implementation of this.

Yep, this would require changes to alter the marshaling process to avoid having the Count field in the final JSON representation.

I think a better Ignores option supporting a JSON Path like syntax could be the solution to ignore nested fields at any level.