Package: drf-flex-fields
Django REST - FlexFieldsFlexible, dynamic fields and nested models for Django REST Framework serializers.OverviewFlexFields (DRF-FF) forDjango REST Frameworkis a package designed to provide a common baseline of functionality for dynamically setting fields and nested models within DRF serializers. This package is designed for simplicity, with minimal magic and entanglement with DRF's foundational classes.Key benefits:Easily set up fields that be expanded to their fully serialized counterparts via query parameters (users/?expand=organization,friends)Select a subset of fields by either:specifying which ones should be included (users/?fields=id,first_name)specifying which ones should be excluded (users/?omit=id,first_name)Use dot notation to dynamically modify fields at arbitrary depths (users/?expand=organization.owner.roles)Flexible API - options can also be passed directly to a serializer:UserSerializer(obj, expand=['organization'])Quick Startfromrest_flex_fieldsimportFlexFieldsModelSerializerclassStateSerializer(FlexFieldsModelSerializer):classMeta:model=Statefields=('id','name')classCountrySerializer(FlexFieldsModelSerializer):classMeta:model=Countryfields=('id','name','population','states')expandable_fields={'states':(StateSerializer,{'many':True})}classPersonSerializer(FlexFieldsModelSerializer):classMeta:model=Personfields=('id','name','country','occupation')expandable_fields={'country':CountrySerializer}GET /people/142/{"id":142,"name":"Jim Halpert","country":1}GET /people/142/?expand=country.states{"id":142,"name":"Jim Halpert","country":{"id":1,"name":"United States","states":[{"id":23,"name":"Ohio"},{"id":2,"name":"Pennsylvania"}]}}Table of Contents:Django REST - FlexFieldsOverviewQuick StartTable of Contents:SetupUsageDynamic Field ExpansionDeferred FieldsDeep, Nested ExpansionField Expansion on "List" ViewsExpanding a "Many" RelationshipDynamically Setting Fields (Sparse Fields)Reference serializer as a string (lazy evaluation)Increased re-usability of serializersSerializer OptionsAdvancedCustomizationSerializer IntrospectionUse Wildcards to Match Multiple FieldsCombining Sparse Fields and Field ExpansionUtility Functionsrest_flex_fields.is_expanded(request, field: str)rest_flex_fields.is_included(request, field: str)Query optimization (experimental)ChangelogTestingLicenseSetupFirst install:pip install drf-flex-fieldsThen have your serializers subclassFlexFieldsModelSerializer:fromrest_flex_fieldsimportFlexFieldsModelSerializerclassStateSerializer(FlexFieldsModelSerializer):classMeta:model=Countryfields=('id','name')classCountrySerializer(FlexFieldsModelSerializer):classMeta:model=Countryfields=('id','name','population','states')expandable_fields={'states':(StateSerializer,{'many':True})}Alternatively, you can add theFlexFieldsSerializerMixinmixin to a model serializer.UsageDynamic Field ExpansionTo define expandable fields, add anexpandable_fieldsdictionary to your serializer'sMetaclass. Key the dictionary with the name of the field that you want to dynamically expand, and set its value to either the expanded serializer or a tuple where the first element is the serializer and the second is a dictionary of options that will be used to instantiate the serializer.classCountrySerializer(FlexFieldsModelSerializer):classMeta:model=Countryfields=['name','population']classPersonSerializer(FlexFieldsModelSerializer):country=serializers.PrimaryKeyRelatedField(read_only=True)classMeta:model=Personfields=['id','name','country','occupation']expandable_fields={'country':CountrySerializer}If the default serialized response is the following:{"id":13322,"name":"John Doe","country":12,"occupation":"Programmer"}When you do aGET /person/13322?expand=country, the response will change to:{"id":13322,"name":"John Doe","country":{"name":"United States","population":330000000},"occupation":"Programmer"}Deferred FieldsAlternatively, you could treatcountryas a "deferred" field by not defining it among the default fields. To make a field deferred, only define it within the serializer'sexpandable_fields.Deep, Nested ExpansionLet's say you addStateSerializeras a serializer nested inside the country serializer above:classStateSerializer(FlexFieldsModelSerializer):classMeta:model=Statefields=['name','population']classCountrySerializer(FlexFieldsModelSerializer):classMeta:model=Countryfields=['name','population']expandable_fields={'states':(StateSerializer,{'many':True})}classPersonSerializer(FlexFieldsModelSerializer):country=serializers.PrimaryKeyRelatedField(read_only=True)classMeta:model=Personfields=['id','name','country','occupation']expandable_fields={'country':CountrySerializer}Your default serialized response might be the following forpersonandcountry, respectively:{"id":13322,"name":"John Doe","country":12,"occupation":"Programmer",}{"id":12,"name":"United States","states":"http://www.api.com/countries/12/states"}But if you do aGET /person/13322?expand=country.states, it would be:{"id":13322,"name":"John Doe","occupation":"Programmer","country":{"id":12,"name":"United States","states":[{"name":"Ohio","population":11000000}]}}Please be kind to your database, as this could incur many additional queries. Though, you can mitigate this impact through judicious use ofprefetch_relatedandselect_relatedwhen defining the queryset for your viewset.Field Expansion on "List" ViewsIf you request many objects, expanding fields could lead to many additional database queries. SubclassFlexFieldsModelViewSetif you want to prevent expanding fields by default when calling a ViewSet'slistmethod. Place those fields that you would like to expand in apermit_list_expandsproperty on the ViewSet:fromrest_flex_fieldsimportis_expandedclassPersonViewSet(FlexFieldsModelViewSet):permit_list_expands=['employer']serializer_class=PersonSerializerdefget_queryset(self):queryset=models.Person.objects.all()ifis_expanded(self.request,'employer'):queryset=queryset.select_related('employer')returnquerysetNotice how this example is using theis_expandedutility method as well asselect_relatedandprefetch_relatedto efficiently query the database if the field is expanded.Expanding a "Many" RelationshipSetmanytoTruein the serializer options to make sure "to many" fields are expanded correctly.classStateSerializer(FlexFieldsModelSerializer):classMeta:model=Statefields=['name','population']classCountrySerializer(FlexFieldsModelSerializer):classMeta:model=Countryfields=['name','population']expandable_fields={'states':(StateSerializer,{'many':True})}A request toGET /countries?expand=stateswill return:{"id":12,"name":"United States","states":[{"name":"Alabama","population":11000000},//...morestates...//{"name":"Ohio","population":11000000}]}Dynamically Setting Fields (Sparse Fields)You can use either thefieldsoromitkeywords to declare only the fields you want to include or to specify fields that should be excluded.Consider this as a default serialized response:{"id":13322,"name":"John Doe","country":{"name":"United States","population":330000000},"occupation":"Programmer","hobbies":["rock climbing","sipping coffee"]}To whittle down the fields via URL parameters, simply add?fields=id,name,countryto your requests to get back:{"id":13322,"name":"John Doe","country":{"name":"United States","population":330000000}}Or, for more specificity, you can use dot-notation,?fields=id,name,country.name:{"id":13322,"name":"John Doe","country":{"name":"United States"}}Or, if you want to leave out the nested country object, do?omit=country:{"id":13322,"name":"John Doe","occupation":"Programmer","hobbies":["rock climbing","sipping coffee"]}Reference serializer as a string (lazy evaluation)To avoid circular import problems, it's possible to lazily evaluate a string reference to you serializer class using this syntax:expandable_fields={'record_set':('<module_path_to_serializer_class>.RelatedSerializer',{'many':True})}Note:
Prior to version0.9.0, it was assumed your serializer classes would be in a module with the following path:<app_name>.serializers.This import style will still work, but you can also now specify fully-qualified import paths to any locations.Increased re-usability of serializersTheomitandfieldsoptions can be passed directly to serializers. Rather than defining a separate, slimmer version of a regular serializer, you can re-use the same serializer and declare which fields you want.fromrest_flex_fieldsimportFlexFieldsModelSerializerclassCountrySerializer(FlexFieldsModelSerializer):classMeta:model=Countryfields=['id','name','population','capital','square_miles']classPersonSerializer(FlexFieldsModelSerializer):country=CountrySerializer(fields=['id','name'])classMeta:model=Personfields=['id','name','country']serializer=PersonSerializer(person)print(serializer.data)>>>{"id":13322,"name":"John Doe","country":{"id":1,"name":"United States",}}Serializer OptionsDynamic field options can be passed in the following ways:from the request's query parameters; separate multiple values with a commmaas keyword arguments directly to the serializer class when its constructedfrom a dictionary placed as the second element in a tuple when definingexpandable_fieldsApproach #1GET /people?expand=friends.hobbies,employer&omit=ageApproach #2serializer=PersonSerializer(person,expand=["friends.hobbies","employer"],omit="friends.age")Approach #3classPersonSerializer(FlexFieldsModelSerializer)://YourfielddefinitionsclassMeta:model=Personfields=["age","hobbies","name"]expandable_fields={'friends':('serializer.FriendSerializer',{'many':True,"expand":["hobbies"],"omit":["age"]})}OptionDescriptionexpandFields to expand; must be configured in the serializer'sexpandable_fieldsfieldsFields that should be included; all others will be excludedomitFields that should be excluded; all others will be includedAdvancedCustomizationParameter names and wildcard values can be configured within a Django setting, namedREST_FLEX_FIELDS.OptionDescriptionDefaultEXPAND_PARAMThe name of the parameter with the fields to be expanded"expand"MAXIMUM_EXPANSION_DEPTHThe max allowed expansion depth. By default it's unlimited. Expandingstate.townswould equal a depth of 2NoneFIELDS_PARAMThe name of the parameter with the fields to be included (others will be omitted)"fields"OMIT_PARAMThe name of the parameter with the fields to be omitted"omit"RECURSIVE_EXPANSION_PERMITTEDIfFalse, an exception is raised when a recursive pattern is foundTrueWILDCARD_VALUESList of values that stand in for all field names. Can be used with thefieldsandexpandparameters.When used withexpand, a wildcard value will trigger the expansion of allexpandable_fieldsat a given level.When used withfields, all fields are included at a given level. For example, you could passfields=name,state.*if you have a city resource with a nested state in order to expand only the city's name field and all of the state's fields.To disable use of wildcards, set this setting toNone.["*", "~all"]For example, if you want your API to work a bit more likeJSON API, you could do:REST_FLEX_FIELDS={"EXPAND_PARAM":"include"}Defining Expansion and Recursive Limits on Serializer ClassesAmaximum_expansion_depthinteger property can be set on a serializer class.recursive_expansion_permittedboolean property can be set on a serializer class.Both settings raiseserializers.ValidationErrorwhen conditions are met but exceptions can be customized by overriding therecursive_expansion_not_permittedandexpansion_depth_exceededmethods.Serializer IntrospectionWhen using an instance ofFlexFieldsModelSerializer, you can examine the propertyexpanded_fieldsto discover which fields, if any, have been dynamically expanded.Use of Wildcard to Match All FieldsYou can passexpand=*(or another value of your choosing) to automatically expand all fields that are available for expansion at a given level. To refer to nested resources, you can use dot-notation. For example, requestingexpand=menu.sectionsfor a restaurant resource would expand its nestedmenuresource, as well as that menu's nestedsectionsresource.Or, when requesting sparse fields, you can passfields=*to include only the specified fields at a given level. To refer to nested resources, you can use dot-notation. For example, if you have anorderresource, you could request all of its fields as well as only two fields on its nestedrestaurantresource with the following:fields=*,restaurent.name,restaurant.address&expand=restaurant.Combining Sparse Fields and Field ExpansionYou may be wondering how things work if you use both theexpandandfieldsoption, and there is overlap. For example, your serialized person model may look like the following by default:{"id":13322,"name":"John Doe","country":{"name":"United States"}}However, you make the following requestHTTP GET /person/13322?include=id,name&expand=country. You will get the following back:{"id":13322,"name":"John Doe"}Thefieldsparameter takes precedence overexpand. That is, if a field is not among the set that is explicitly alllowed, it cannot be expanded. If such a conflict occurs, you will not pay for the extra database queries - the expanded field will be silently abandoned.Utility Functionsrest_flex_fields.is_expanded(request, field: str)Checks whether a field has been expanded via the request's query parameters.Parametersrequest: The request objectfield: The name of the field to checkrest_flex_fields.is_included(request, field: str)Checks whether a field has NOT been excluded via either theomitparameter or thefieldsparameter.Parametersrequest: The request objectfield: The name of the field to checkQuery optimization (experimental)An experimental filter backend is available to help you automatically reduce the number of SQL queries and their transfer size.This feature has not been tested thorougly and any help testing and reporting bugs is greatly appreciated.You can add FlexFieldFilterBackend toDEFAULT_FILTER_BACKENDSin the settings:# settings.pyREST_FRAMEWORK={'DEFAULT_FILTER_BACKENDS':('rest_flex_fields.filter_backends.FlexFieldsFilterBackend',# ...),# ...}It will automatically callselect_relatedandprefetch_relatedon the current QuerySet by determining which fields are needed from many-to-many and foreign key-related models. For sparse fields requests (?omit=fieldX,fieldYor?fields=fieldX,fieldY), the backend will automatically callonly(*field_names)using only the fields needed for serialization.WARNING:The optimization currently works only for one nesting level.Changelog1.0.2 (March 2023)Adds control over whether recursive expansions are allowed and allows setting the max expansion depth. Thanks @andruten!1.0.1 (March 2023)Various bug fixes. Thanks @michaelschem, @andruten, and @erielias!1.0.0 (August 2022)Improvements to the filter backends for generic foreign key handling and docs generation. Thanks @KrYpTeD974 and @michaelschem!0.9.9 (July 2022)Fixes bug inFlexFieldsFilterBackend. Thanks @michaelschem!AddsFlexFieldsDocsFilterBackendfor schema population. Thanks @Rjevski!0.9.8 (April 2022)Set expandable fields as the default example for expand query parameters incoreapi.Field. Thanks @JasperSui!0.9.7 (January 2022)Includes m2m in prefetch_related clause even if they're not expanded. Thanks @pablolmedorado and @ADR-007!0.9.6 (November 2021)Make it possible to use wildcard values with sparse fields requests.0.9.5 (October 2021)Adds OpenAPI support. Thanks @soroush-tabesh!Updates tests for Django 3.2 and fixes deprecation warning. Thanks @giovannicimolin!0.9.3 (August 2021)Fixes bug where custom parameter names were not passed when constructing nested serializers. Thanks @Kandeel4411!0.9.2 (June 2021)Ensurescontextdict is passed down to expanded serializers. Thanks @nikeshyad!0.9.1 (June 2021)No longer auto removessourceargument if it's equal to the field name.0.9.0 (April 2021)Allows fully qualified import strings for lazy serializer classes.0.8.9 (February 2021)Adds OpenAPI support to experimental filter backend. Thanks @LukasBerka!0.8.8 (September 2020)Django 3.1.1 fix. Thansks @NiyazNz!Docs typo fix. Thanks @zakjholt!0.8.6 (September 2020)Addsis_includedutility function.0.8.5 (May 2020)Adds options to customize parameter names and wildcard values. Closes #10.0.8.1 (May 2020)Fixes #44, related to the experimental filter backend. Thanks @jsatt!0.8.0 (April 2020)Adds support forexpand,omitandfieldsquery parameters for non-GET requests.The common use case is creating/updating a model instance and returning a serialized response with expanded fieldsThanks @kotepillar for raising the issue (#25) and @Crocmagnon for the idea of delaying field modification toto_representation().0.7.5 (February 2020)Simplifies declaration ofexpandable_fieldsIf using a tuple, the second element - to define the serializer settings - is now optional.Instead of a tuple, you can now just use the serializer class or a string to lazily reference that class.Updates documentation.0.7.0 (February 2020)Adds support for different ways of passing arrays in query strings. Thanks @sentyaev!Fixes attribute error when map is supplied to split levels utility function. Thanks @hemache!0.6.1 (September 2019)Adds experimental support for automatically SQL query optimization via aFlexFieldsFilterBackend. Thanks ADR-007!Adds CircleCI config file. Thanks mikeIFTS!Moves declaration ofexpandable_fieldstoMetaclass on serialzer for consistency with DRF (will continue to support declaration as class property)Python 2 is no longer supported. If you need Python 2 support, you can continue to use older versions of this package.0.5.0 (April 2019)Added support foromitkeyword for field exclusion. Code clean up and improved test coverage.0.3.4 (May 2018)Handle case whererequestisNonewhen accessing request object from serializer. Thanks @jsatt!0.3.3 (April 2018)ExposesFlexFieldsSerializerMixinin addition toFlexFieldsModelSerializer. Thanks @jsatt!TestingTests are found in a simplified DRF project in the/testsfolder. Install the project requirements and do./manage.py testto run them.LicenseSeeLicense.