14👍
I’m not convinced this is the simplest way, but the solution i came up with was to override the to_native method of the MembershipSerializer and then manually create and invoke the to_native method of the PersonSerializer and merge the two resulting dictionary’s together
class MembershipSerializer(serializers.HyperlinkedModelSerializer):
def to_native(self, obj):
ret = super(MembershipSerializer, self).to_native(obj)
p_serializer = PersonSerializer(obj.person, context=self.context)
p_ret = p_serializer.to_native(obj.person)
for key in p_ret:
ret[key] = p_ret[key]
return ret
class Meta:
model = Membership
fields = ('id', 'url', 'group', 'date_joined', 'invite_reason', 'person')
The dictionary’s are both a subclass of SortedDict. I’m not sure whether there’s an explicit method to merge the two that preserves the order, so i’ve just used a loop instead.
61👍
Here’s an approach based on James’s answer but for a newer version of Django Rest Framework and support for reading and writing (update of the nested field only, it should be easy enough to add creation, see DRF’s documentation for that.)
class ProfileSerializer(serializers.ModelSerializer):
class Meta:
model = Profile
fields = ('phone', 'some', 'other', 'fields')
class UserDetailsSerializer(serializers.ModelSerializer):
"""User model with Profile. Handled as a single object, profile is flattened."""
profile = ProfileSerializer()
class Meta:
model = User
fields = ('username', 'email', 'profile')
read_only_fields = ('email', )
def to_representation(self, obj):
"""Move fields from profile to user representation."""
representation = super().to_representation(obj)
profile_representation = representation.pop('profile')
for key in profile_representation:
representation[key] = profile_representation[key]
return representation
def to_internal_value(self, data):
"""Move fields related to profile to their own profile dictionary."""
profile_internal = {}
for key in ProfileSerializer.Meta.fields:
if key in data:
profile_internal[key] = data.pop(key)
internal = super().to_internal_value(data)
internal['profile'] = profile_internal
return internal
def update(self, instance, validated_data):
"""Update user and profile. Assumes there is a profile for every user."""
profile_data = validated_data.pop('profile')
super().update(instance, validated_data)
profile = instance.profile
for attr, value in profile_data.items():
setattr(profile, attr, value)
profile.save()
return instance
- [Django]-Django: best practice way to get model from an instance of that model
- [Django]-ChoiceField in Django model
- [Django]-How can I allow django admin to set a field to NULL?
20👍
James’ answer is what I finally used. As I had several serializers using this method, I converted it to a mixin:
class FlattenMixin(object):
"""Flatens the specified related objects in this representation"""
def to_representation(self, obj):
assert hasattr(self.Meta, 'flatten'), (
'Class {serializer_class} missing "Meta.flatten" attribute'.format(
serializer_class=self.__class__.__name__
)
)
# Get the current object representation
rep = super(FlattenMixin, self).to_representation(obj)
# Iterate the specified related objects with their serializer
for field, serializer_class in self.Meta.flatten:
serializer = serializer_class(context = self.context)
objrep = serializer.to_representation(getattr(obj, field))
#Include their fields, prefixed, in the current representation
for key in objrep:
rep[field + "__" + key] = objrep[key]
return rep
This way, you can do something like:
class MembershipSerializer(FlattenMixin, serializers.HyperlinkedModelSerializer):
class Meta:
model = Membership
fields = ('id', 'url', 'group', 'date_joined', 'invite_reason')
flatten = [ ('person', PersonSerializer) ]
- [Django]-Django set default form values
- [Django]-Allowing only super user login
- [Django]-Django model CharField: max_length does not work?
6👍
I didn’t try it with HyperlinkedModelSerializer
but with ModelSerializer
you can make custom serializer class that supports flatten
option.
class CustomModelSerializer(serializers.ModelSerializer):
def __init__(self, *args, **kwargs):
self.flatten = kwargs.pop('flatten', False)
super(CustomModelSerializer, self).__init__(*args, **kwargs)
def get_fields(self):
fields = super(CustomModelSerializer, self).get_fields()
for field_name, field in fields.items():
if getattr(field, 'flatten', False):
del fields[field_name]
for nested_field_name, nested_field in field.fields.iteritems():
nested_field.source = (field_name + '.' +
(nested_field.source or nested_field_name))
fields[nested_field_name] = nested_field
return fields
Usage:
class MembershipSerializer(CustomModelSerializer):
person = PersonSerializer(flatten=True)
class Meta:
model = Membership
fields = ('person', ...)
class PersonSerializer(CustomModelSerializer):
class Meta:
model = Person
fields = (...)
- [Django]-How to get the username of the logged-in user in Django?
- [Django]-Django applying a style class based on a conditional
- [Django]-Django 'Sites' Model – what is and why is 'SITE_ID = 1'?
0👍
combining ekuusela‘s answer and this example from the DRF documentatation, you can also control which fields (from the nested object) you want to display.
Your serializer would look like this
class UserDetailsSerializer(serializers.ModelSerializer):
"""User model with Profile. Handled as a single object, profile is flattened."""
profile = ProfileSerializer()
def __init__(self, *args, **kwargs):
self.allow_fields = kwargs.pop('fields', None)
super(ProfileSerializer, self).__init__(*args, **kwargs)
class Meta:
model = User
fields = ('username', 'email', 'profile')
def to_representation(self, instance):
representation = super().to_representation(instance)
profile_representation = representation.pop('profile')
representation.update(profile_representation)
if self.allow_fields is not None:
# Drop any fields that are not specified in the `fields` argument.
allowed = set(self.allow_fields)
existing = set(representation)
for field_name in existing - allowed:
representation.pop(field_name)
return representation
And you would instantiate your Serializer as if it was only a singe Model
serializer = UserDetailsSerializer(user, fields=('username', 'email','profile_field1', 'profile_field2'))
- [Django]-Get request data in Django form
- [Django]-Form field description in django admin
- [Django]-How to debug in Django, the good way?
0👍
Here’s another solution to add the fields dynamically thanks to meta classes!
class FlattenNestedFields(SerializerMetaclass):
def __new__(cls, name, bases, attrs):
klass = super().__new__(cls, name, bases, attrs)
for related_name, fields in attrs['related_fields']:
klass.Meta.fields += fields
for field in fields:
field_rhs = ReadOnlyField(source=f'{related_name}.{field}')
klass._declared_fields[field] = field_rhs
setattr(klass, field, field_rhs)
return klass
Now, when defining your serializer, all you need to do is this:
class MembershipSerializer(serializers.HyperlinkedModelSerializer, metaclass=FlattenNestedFields):
related_fields = [
('person', ['first_name', 'last_name', 'favourite_food']),
]
class Meta:
model = Membership
fields = ('id', 'url', 'group', 'date_joined', 'invite_reason',)
and the attributes (as well as the Meta
inner class fields) are generated by the metaclass.
- [Django]-Row level permissions in django
- [Django]-Change a field in a Django REST Framework ModelSerializer based on the request type?
- [Django]-How to get URL of current page, including parameters, in a template?