Django Rest Framework serialize query set into dictionary with custom field - json

I'm trying to get this kind of JSON:
"Physical": {
"Strength": 1,
"Dexterity": 1,
"Stamina": 1
},
But with my Custom serializer:
class EnumField(serializers.Field):
"""
Enum objects are serialized into " 'label' : value " notation
"""
def to_representation(self, obj):
return {"{0}".format(obj.all()[0].__str__()): obj.all()[0].current_value}
class EnumListField(serializers.DictField):
child = EnumField()
And this on my model:
#property
def physical_attributes(self):
return [self.attributes.filter(attribute=attribute) for attribute
in AttributeAbility.objects.physical()]
Outputs this:
"mental_attributes": [
{
"Intelligence": 1
},
{
"Wits": 0
},
{
"Resolve": 0
}
],
What do I need to do to my field, to look like my first JSON? I don't think DictField exists anymore, which is what a few questions on SO suggested.

Your property returns a list, and you want key:value pairs, so treat them as a dictionary:
def to_representation(self, obj):
return {str(item.get()): item.get().value for item in obj}
Obviously replace .value with whatever value you want, and if the str() representation is not what you want for the key, replace that.

Related

Mongo Embedded Document is not JSON Serializable - Python

I am currently using Graphene with MongoEngine. The mongo db schemas are as follows
class DocumentAModel(Document):
id = StringField(required=True)
documentB = MapField(EmbeddedDocumentField(DocumentBModel)
class DocumentBModel(EmbeddedDocument):
id = StringField(required=True)
value = IntField()
A sample documentA would be as following
{
id: "id_1",
documentB: {
0: {
id: "b_1",
value: 1
},
1: {
id: "b_2",
value: 11
}
}
And correspondingly, their Graphene types are
class DocumentB(MongoengineObjectType):
class Meta:
model = DocumentBModel
class DocumentA(MongoengineObjectType):
class Meta:
model = DocumentAModel
Finally, the query looks like the following
class Query(graphene.ObjectType):
all_document_a = graphene.List(DocumentA)
def resolve_all_document_a(self, info):
return list(DocumentAModel.objects.all())
However, when I query allDocumentA to get document B, i get the error
Object of type DocumentBModel is not JSON serializable
I am not sure where to marshal Document B to json.
If I change DocumentB from MapField(EmbeddedDocumentField(DocumentBModel) to DictField(), it works without issue. But is there a way to use MapField?
Thanks
MapField is Similar to a DictField, except the ‘value’ of each item must match the specified field type, and keys must be String type.
So in your case DocumentAModel documentB field is MapField and type of value is DocumentBModel. So for DocumentBModel you need to provide id and value fields. You are creating this using graphene but the model mapping will be same for normal(REST) DRF API and GraphQL API. MapField has some validations like key should be String type and value should be type which you have mentioned in model, but in case of Dictfield, it does not needed such type validations, its just normal Python Dictionary field.
Check the code snippet. And change the Graphene query and schema as per requirement.
Check the following code:
class DocumentBModel(fields.EmbeddedDocument):
id = fields.StringField(required=True)
value = fields.IntField()
class DocumentAModel(Document):
name = fields.StringField(required=True)
documentB = fields.MapField(fields.EmbeddedDocumentField(DocumentBModel))
Django shell
$ python manage.py shell
>>>
>>> B_obj1 = DocumentBModel(**{'id': 'b_1', 'value': 1})
>>> B_obj2 = DocumentBModel(**{'id': 'b_2', 'value': 2})
>>> data_obj = DocumentAModel.objects.create(**{"name":"akash", "documentB":{"0":B_obj1, "1":B_obj2}})
>>> data_obj._data
{'id': ObjectId('5ebd1d2cf549becd5a462924'), 'name': 'akash', 'documentB': {'0': <DocumentBModel: DocumentBModel object>, '1': <DocumentBModel: DocumentBModel object
>}}
Database entry:
{
"_id" : ObjectId("5ebd1d2cf549becd5a462924"),
"name" : "akash",
"documentB" : {
"0" : {
"id" : "b_1",
"value" : 1
},
"1" : {
"id" : "b_2",
"value" : 2
}
}
}
I think the issue is because graphql requires the response object to have a definite structure. Having a dict or a map as an attribute "confuses" graphql.
In the example above, the query could be
query {
documentA {
id
documentB {
0 {
id
value
}
1 {
id
value
}
}
}
}
but documentB doesnt have any attributes 0 and 1. So it just have to be a list of dicts. In which case, the query will become
query {
documentA {
id
documentB {
id
value
}
}
}

How to return json in python graphene resolver without backslashes before quotation marks

I have a backend server in python (Flask + Graphene) and I need to return a JSON object like this:
{
's1': "Section 1",
's2': "Section 2",
's3': "Section 3",
's4': "Section 4"
}
The resolver looks like below:
questionnaire = graphene.types.json.JSONString(
description='JSON result test')
def resolve_questionnaire(self, info: graphql.ResolveInfo):
sections = {
's1': "Section 1",
's2': "Section 2",
's3': "Section 3",
's4': "Section 4"
}
print(json.dumps(sections))
return sections
and in console I see as a result of print(json.dumps(sections)) as I expect:
user-api_1 | {"s1": "Section 1", "s2": "Section 2", "s3": "Section 3", "s4": "Section 4"}
But in GraphiQL i see all quotation marks with backslashes:
When I change the return sections to return json.dumps(sections) I get the result like this:
The question is how to properly return a JSON object in graphene resolver?
I know that there is json.replace method to use like here, but I believe that I am simply producing/passing object in wrong way.
Your initial result of
{
"data": {
"questionnaire": "{\"s1\": \"Section 1\", \"s2\": \"Section 2\", \"s3\": \"Section 3\", \"s4\": \"Section 4\"}"
}
}
is the intended behavior. After all, questionnaire resolves to a JSONString. Since it is a string it must be double quoted, thus its inner quotations must be escaped. This is according to JSON's standards.
To use that string you, would have to run some sort of JSON parser on the data.questionnaire object. In javascript, for instance, it would be something like:
var data;
// Fetching logic to get the data object from your GraphQL server
var sections = JSON.parse(data.questionaire);
// Now you can access its objects
console.log(sections.s1) // Should print "Section 1" on the dev console
However, the method described above is not ideal if the keys of sections are not predetermined (sections.s5 may be defined in one case but undefined in another). Instead, you might rather have an array that you can iterate over. To do this, you would have to define a "model" that has explicit key-value pairs. Doing this way is format suited for GraphQL, too. For instance:
import graphene
# Our new model
class Section(graphene.ObjectType):
key = graphene.String() # dictionary key
header = graphene.String() # dictionary value
# Your previous schema with modifications
class Query(graphene.ObjectType):
# questionnaire = graphene.types.json.JSONString(description='JSON result test')
# Return a list of section objects
questionnaire = graphene.List(Section)
def resolve_questionnaire(self, info: graphql.ResolveInfo):
sections = {
's1': "Section 1",
's2': "Section 2",
's3': "Section 3",
's4': "Section 4"
}
sections_as_obj_list = [] # Used to return a list of Section types
# Create a new Section object for each item and append it to list
for key, value in sections.items(): # Use sections.iteritems() in Python2
section = Section(key, value) # Creates a section object where key=key and header=value
sections_as_obj_list.append(section)
# return sections
return sections_as_obj_list
Now, if we run the query:
query {
questionnaire {
key
header
}
}
It returns a JSON array that can be iterated through.
{
"data" {
"questionnaire": [
{
"key": "s1",
"header": "Section 1"
},
{
"key": "s2",
"header": "Section 2"
},
{
"key": "s3",
"header": "Section 3"
},
{
"key": "s4",
"header": "Section 4"
},
]
}
}
Graphene now has a GenericScalar type for this.
from graphene.types import generic
...
errors = generic.GenericScalar()
You can just subclass the JSON type and replace the serialize method:
class Any(JSON):
#staticmethod
def serialize(dt):
return dt
Then instead of
questionnaire = Field(JSON)
write
questionnaire = Field(Any)
Yes, this does break the strictly-typed spirit of GraphQL, but if that's what you want to do, there's how to do it. Note that this is an output-only hack — it won't allow you to accept arbitrary structures as arguments.
In my case I have the JSON column called (details).
from graphene.types.scalars import Scalar
class ObjectField(Scalar):
''' convert the Json String into Json '''
#staticmethod
def serialize(dt):
return dt
#staticmethod
def parse_literal(node):
return node.value
#staticmethod
def parse_value(value):
return value
class CustomDiseaseFactNode(graphene.Node):
class Meta:
name = 'diseaseFactNode'
#staticmethod #this class used to get the primary key object id
def to_global_id(type, id):
return id
Call the JSONScalar from your object class
class DiseaseFactNode(SQLAlchemyObjectType):
"""DiseaseFact node."""
class Meta:
model = DiseaseFact
interfaces = (CustomDiseaseFactNode,)
details = JSONScalar()
Another approach, to return an array of json / dictionary.
I was trying to return json from a model mixin property attribute.
Basically an any type ( note: loses the benefits of typing ), helped me to return json from a list of dictionaries:
import graphene
from graphene import relay, Scalar
from graphene_django import DjangoObjectType
class DictType(Scalar):
#staticmethod
def serialize(dt):
return dt
#staticmethod
def parse_literal(node):
return node
#staticmethod
def parse_value(value):
return value
The node itself was based on a model which included a mixin:
class InvoiceFileNode(DjangoObjectType):
signed_url = graphene.String()
variable_files = graphene.List(of_type=DictType)
class Meta:
model = InvoiceFile
interfaces = (relay.Node,)
filter_fields = []
only_fields = (
"id",
"created",
"signed_url",
"variable_files",
)
def resolve_signed_url(self, *args, **kwargs):
# #property access from FileMixin
return self.signed_url
def resolve_openable_signed_url(self, *args, **kwargs):
# #property access from FileMixin
return self.openable_signed_url
The following mixin was what I was trying to get returned, but using the of_type of JSONString serialized the dictionary to a json string:
class FileMixin(object):
#property
def signed_url(self) -> str:
return get_signed_url(self.file)
#property
def variable_files(self) -> str:
sizes = []
for s in [128, 240, 720]:
item = {"width": s}
urls = []
for n in [1,2,3]:
urls.append(get_sized_url(self.file, n))
item["urls"] = urls
sizes.append(item)
return sizes
class InvoiceFile(Models, FileMixin):
created = DateTimeField()
file = CharField()
I was having issues with returning something like:
[{"width": 123, "stuff": [{"more": "stuff"}]}}
note
this will probably not work if the dictionary returned contains any sorts of functions, or objects, etc.

Django RestFramework returning a precomputed json

Currently I have this:
class MySerializer(serializers.ModelSerializer):
class Meta:
model = MyModel
fields = (
'id', 'f0', 'f1', 'f2')
And it returns something like this:
{
"count": 6242,
"previous": null,
"total_pages": 209,
"results": [
{
"id": 63915,
"f0": "Some stuff"
.....
},
{
"id": 63916,
"f0": "Some other stuff"
.....
}....
]
}
And this is good, but I noticed that serializing the data is actually quite expensive to do on the fly, so I would like to precompute it. So far I've managed to precompute it and store it in a jsonfield for my model, the problem is my API is now returning {'json_repersentation':{myold_response}}
class MySerializer(serializers.ModelSerializer):
class Meta:
model = MyModel
fields = ('json_representation',)
My question is, is it possible to change it so that it simply returns the json contained within json_representation field without the "overhead" of {'json_representation':{id:0, f0:label...}} and instead just simply {id:0, f0:label...}
You can override the serializer to_representation method:
def to_representation(self, instance):
data = super(MySerializer, self).to_representation(instance)
return data['json_representation']

Change structure of serialized object in Django Rest Framework (pull ID field outside of model content)

Say, you have a model with name and e-mail and used Model Serializer to serialize your data with Django Rest Framework. The output would be something like this:
[{
"id": "1",
"name": "Alex",
"email": "alex#host.com"
}, {
"id": "2",
"name": "Henry",
"email": "henry#host.com"
}]
Is there a way to "pull" a unique field outside the rest of the model content to get this?:
{
"1": {
"name": "Alex",
"email": "alex#host.com"
},
"2": {
"name": "Henry",
"email": "henry#host.com"
}
}
I am able to "externalize" the ID by overriding serializer's to_representation() method (code sample below) but I can't get rid of the rudimental wrappers - currently JRF will return everything as list[] of dict(instance) similar to this:
data = list
for item in instance:
list.append(item.as_dict())
I don't need this to be a list of dicts, I want them to do be dict of dict?
data = {}
for item in instance:
data[item.id] = item.as_dict()
here's my to_representation() code, I can only manipulate what's returned for individual instance, so it has to be a dict, but I need this dict to be merged, rather than stacked at the output when serializer.data is fully cooked.
def to_representation(self, instance):
rep = OrderedDict()
fields = self._readable_fields
id_field = filter(lambda i: i.label == "ID", fields)[0]
if id_field:
id_atr = id_field.get_attribute(instance).__str__()
else:
raise Exception('cannot serialize models without ID field')
rep[id_atr] = {}
for field in fields:
try:
attribute = field.get_attribute(instance)
except SkipField:
continue
if attribute is None:
rep[id_atr][field.field_name] = None
else:
rep[id_atr][field.field_name] = field.to_representation(attribute)
return rep
I was able to make it work by reformatting the original serializer output, but it's surely not a clean solution with a lot of performance impact:
def list(self, request, *args, **kwargs):
_data = super(ChatMessageViewSet, self).list(request, *args, **kwargs)
_resp = {}
for item in _data.data:
_id = item.get('id', None)
if not _id:
raise Exception('cannot serialize data without id field')
_resp[_id] = {}
for element in item:
_resp[_id][element] = item[element]
_data.data = _resp
return _data
as per #zaphod100-10 answer, I have created a custom ListSerializer class:
class ChatMessageListSerializer(serializers.ListSerializer):
def to_representation(self, data):
iterable = data.all() if isinstance(data, models.Manager) else data
_data = {}
for item in iterable:
_data[item.id] = self.child.to_representation(item)
return _data
and it formats the data the way I need this:
but my data does not survive 'ListModelMixin':
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)
it returns a list[] of just the entity ids: [1L, 2L, 3L...]
Create a custom ListSerializer for your model and override the to_representation method.
def to_representation(self, data):
# convert your data which is a queryset or a
# list of objects to dict of dicts
.....
return dict_of_dicts
After creating the ListSerializer in your main serializer class add list_serializer_class meta property.
read more about list serializer here:
http://www.django-rest-framework.org/api-guide/serializers/#listserializer

updating Json object

I have a json object that I need to update. The original object is a list that looks like this:
[
{
"firstName":"Jane",
"lastName":"Smith"
},
{
"firstName":"Jack",
"lastName":"Brown"
}
]
For each element in the list, we have an extra field, "age", that needs to be added at run-time, so the result should look like the following:
[
{
"firstName":"Jane",
"lastName":"Smith",
"age": "21"
},
{
"firstName":"Jack",
"lastName":"Brown",
"age": "34"
}
]
Any suggestions how to do this so the result is still json?
Thanks.
request.body.asJson.map {
jm => (jm.as[JsObject] ++ Json.obj("age" -> 123))
}
I would recommended deserializing the JSON array you receive into a List of case classes, then having some function fill in the missing attributes based on the current attributes of the case class, and finally serializing them as JSON and serving the response.
Let's make a Person case class with the fields that will be missing as Option:
import play.api.libs.json.Json
case class Person(firstName: String, lastName: String, age: Option[Int])
object Person {
implicit val format: Format[Person] = Json.format[Person]
def addAge(person: Person): Person = {
val age = ... // however you determine the age
person.copy(age = Some(age))
}
}
Within the companion object for Person I've also defined a JSON serializer/deserializer using the format macro, and a stub for a function that will find a person's age then copy it back into the person and return it.
Deep within the web service call you might then have something like this:
val jsArray = ... // The JsValue from somewhere
jsArray.validate[List[Person]].fold(
// Handle the case for invalid incoming JSON
error => InternalServerError("Received invalid JSON response from remote service."),
// Handle a deserialized array of List[Person]
people => {
Ok(
// Serialize as JSON, requires the implicit `format` defined earlier.
Json.toJson(
// Map each Person to themselves, adding the age
people.map(person => Person.addAge(person))
)
)
}
)
This method is much safer, otherwise you'll have to extract values from the array one by one and concatenate objects, which is very awkward. This will also allow you to easily handle errors when the JSON you receive is missing fields you're expecting.