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

Polymorphic JSON field #2491

Closed
kpeu3i opened this issue Apr 23, 2022 · 6 comments
Closed

Polymorphic JSON field #2491

kpeu3i opened this issue Apr 23, 2022 · 6 comments
Labels

Comments

@kpeu3i
Copy link

kpeu3i commented Apr 23, 2022

Hi, guys! I have a question about defining the JSON field which stores different structures depending on the entity type.

I know that I can define JSON like this:

field.JSON("data", Data{})

But in my case, I need to have something like that:

field.JSON("data", Data1{})
field.JSON("data", Data2{})
field.JSON("data", Data3{})

because different structs are stored inside the data field.

So, is it possible somehow to choose the correct data structure when scanning the JSON field based on another field value from the entity (e.g type)?

@a8m a8m added the Question label Apr 25, 2022
@a8m
Copy link
Member

a8m commented Apr 25, 2022

Hey @kpeu3i and thanks for opening this issue 👋

You can use a custom type that implements the json.Marshaler/json.Unmarshaler interfaces, store a field named Data (an interface type) and decide on the decoding stage what type to assign to it.

For example, for a field field.JSON("addr", Addr{}) that can hold multiple variants of "net addresses", I'll implement it as follows:

type Addr struct{ net.Addr }

func (a *Addr) UnmarshalJSON(data []byte) error {
	var types struct {
		TCP *net.TCPAddr `json:"tcp,omitempty"`
		UDP *net.UDPAddr `json:"udp,omitempty"`
	}
	if err := json.Unmarshal(data, &types); err != nil {
		return err
	}
	switch {
	case types.TCP != nil && types.UDP != nil:
		return errors.New("TCP and UDP addresses are mutually exclusive")
	case types.TCP != nil:
		a.Addr = types.TCP
	case types.UDP != nil:
		a.Addr = types.UDP
	}
	return nil
}

func (a Addr) MarshalJSON() ([]byte, error) {
	var types struct {
		TCP *net.TCPAddr `json:"tcp,omitempty"`
		UDP *net.UDPAddr `json:"udp,omitempty"`
	}
	switch a := a.Addr.(type) {
	case *net.TCPAddr:
		types.TCP = a
	case *net.UDPAddr:
		types.UDP = a
	default:
		return nil, fmt.Errorf("unsupported address type: %T", a)
	}
	return json.Marshal(types)
}

I also added an example PR for your use-case in this repository, please give it a look #2497

Closing, but please feel free to reopen or join our Discord server if you still need help with this.

@a8m a8m closed this as completed Apr 25, 2022
@kpeu3i
Copy link
Author

kpeu3i commented Apr 25, 2022

@a8m thanks for your response. I got your idea. But I'm trying to switch to Ent from an existing database and I cannot modify the data structure that is stored inside the data column.

In terms of your example, *net.TCPAddr and *net.UDPAddr are stored in my DB without wrapper struct types, so I cannot detect which struct should be used when unmarshaling bytes.

The only way to detect which struct should be used is the value of type column from the parent entity. So, I'm wondering is it possible somehow access other entity field values when scanning a value for the field? Seems like it should be possible inside the generated assignValues method.

@masseelch
Copy link
Collaborator

masseelch commented Apr 25, 2022

To add my idea to this:

You can use json.RawMessage as type in your schema and add a "getter" method in your entity that can read the type field and parse the JSON when accessed.

// in schema
field.Json("data", json.RawMessage{})

// With 1.18 you can use generics to return a union type
func (s MySchema) Data() Data1 | Data2 | Data3 { 
  switch s.Type {
    case: ....
  }
}

Once read hooks/interceptors are present in Ent, this can be done similar to the mutation hooks.

@a8m
Copy link
Member

a8m commented Apr 25, 2022

I think another nice solution for this is to use field.JSON("data", json.RawMessage{}) to store the field raw, and expose a method on the type T like ParseData() Data that can access the type and the data fields and can return you a parsed version of it. Same thing for Marshaling.

These fields can be added using custom templates or manually in an ent/custom.go file.

We can expose an API for calling custom functions in assignValues, but I need to think of a clean API for this. I'd expect something like this to reside on the field-level but the issue is that you need access to another field

@a8m
Copy link
Member

a8m commented Apr 25, 2022

hehe, @masseelch 😆

@hedwigz and I just discussed this and we both commented this at the same time :)

@kpeu3i
Copy link
Author

kpeu3i commented Apr 26, 2022

Guys, thanks for the help I will follow your suggestion about json.RawMessage.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

3 participants