Skip to content

Commit e22e101

Browse files
authored
Merge pull request #348 from json-api-dotnet/fix/deserializer-independent-has-one
fix(Deserializer): deserialize indpendent hasone pointers
2 parents 9c1f245 + 8d6ef80 commit e22e101

File tree

3 files changed

+51
-28
lines changed

3 files changed

+51
-28
lines changed

src/JsonApiDotNetCore/Models/ResourceIdentifierObject.cs

+8-1
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,16 @@ namespace JsonApiDotNetCore.Models
44
{
55
public class ResourceIdentifierObject
66
{
7+
public ResourceIdentifierObject() { }
8+
public ResourceIdentifierObject(string type, string id)
9+
{
10+
Type = type;
11+
Id = id;
12+
}
13+
714
[JsonProperty("type")]
815
public string Type { get; set; }
9-
16+
1017
[JsonProperty("id")]
1118
public string Id { get; set; }
1219

src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs

+32-26
Original file line numberDiff line numberDiff line change
@@ -204,53 +204,59 @@ private object SetHasOneRelationship(object entity,
204204
if (relationships.TryGetValue(relationshipName, out RelationshipData relationshipData) == false)
205205
return entity;
206206

207-
var relationshipAttr = _jsonApiContext.RequestEntity.Relationships
208-
.SingleOrDefault(r => r.PublicRelationshipName == relationshipName);
209-
210-
if (relationshipAttr == null)
211-
throw new JsonApiException(400, $"{_jsonApiContext.RequestEntity.EntityName} does not contain a relationship '{relationshipName}'");
212-
213207
var rio = (ResourceIdentifierObject)relationshipData.ExposedData;
214208

215209
var foreignKey = attr.IdentifiablePropertyName;
216210
var foreignKeyProperty = entityProperties.FirstOrDefault(p => p.Name == foreignKey);
217-
218211
if (foreignKeyProperty == null && rio == null)
219212
return entity;
220213

221-
if (foreignKeyProperty == null && rio != null)
222-
throw new JsonApiException(400, $"{contextEntity.EntityType.Name} does not contain a foreign key property '{foreignKey}' for has one relationship '{attr.InternalRelationshipName}'");
223-
224-
// e.g. PATCH /articles
225-
// {... { "relationships":{ "Owner": { "data": null } } } }
226-
if (rio == null && Nullable.GetUnderlyingType(foreignKeyProperty.PropertyType) == null)
227-
throw new JsonApiException(400, $"Cannot set required relationship identifier '{attr.IdentifiablePropertyName}' to null because it is a non-nullable type.");
214+
SetHasOneForeignKeyValue(entity, attr, foreignKeyProperty, rio);
215+
SetHasOneNavigationPropertyValue(entity, attr, rio, included);
228216

229-
var newValue = rio?.Id ?? null;
230-
var convertedValue = TypeHelper.ConvertType(newValue, foreignKeyProperty.PropertyType);
217+
return entity;
218+
}
231219

232-
_jsonApiContext.RelationshipsToUpdate[relationshipAttr] = convertedValue;
220+
private void SetHasOneForeignKeyValue(object entity, HasOneAttribute hasOneAttr, PropertyInfo foreignKeyProperty, ResourceIdentifierObject rio)
221+
{
222+
var foreignKeyPropertyValue = rio?.Id ?? null;
223+
if (foreignKeyProperty != null)
224+
{
225+
// in the case of the HasOne independent side of the relationship, we should still create the shell entity on the other side
226+
// we should not actually require the resource to have a foreign key (be the dependent side of the relationship)
233227

234-
foreignKeyProperty.SetValue(entity, convertedValue);
228+
// e.g. PATCH /articles
229+
// {... { "relationships":{ "Owner": { "data": null } } } }
230+
if (rio == null && Nullable.GetUnderlyingType(foreignKeyProperty.PropertyType) == null)
231+
throw new JsonApiException(400, $"Cannot set required relationship identifier '{hasOneAttr.IdentifiablePropertyName}' to null because it is a non-nullable type.");
235232

233+
var convertedValue = TypeHelper.ConvertType(foreignKeyPropertyValue, foreignKeyProperty.PropertyType);
234+
foreignKeyProperty.SetValue(entity, convertedValue);
235+
_jsonApiContext.RelationshipsToUpdate[hasOneAttr] = convertedValue;
236+
}
237+
}
236238

237-
if (rio != null
238-
// if the resource identifier is null, there should be no reason to instantiate an instance
239-
&& rio.Id != null)
239+
/// <summary>
240+
/// Sets the value of the navigation property for the related resource.
241+
/// If the resource has been included, all attributes will be set.
242+
/// If the resource has not been included, only the id will be set.
243+
/// </summary>
244+
private void SetHasOneNavigationPropertyValue(object entity, HasOneAttribute hasOneAttr, ResourceIdentifierObject rio, List<DocumentData> included)
245+
{
246+
// if the resource identifier is null, there should be no reason to instantiate an instance
247+
if (rio != null && rio.Id != null)
240248
{
241249
// we have now set the FK property on the resource, now we need to check to see if the
242250
// related entity was included in the payload and update its attributes
243-
var includedRelationshipObject = GetIncludedRelationship(rio, included, relationshipAttr);
251+
var includedRelationshipObject = GetIncludedRelationship(rio, included, hasOneAttr);
244252
if (includedRelationshipObject != null)
245-
relationshipAttr.SetValue(entity, includedRelationshipObject);
253+
hasOneAttr.SetValue(entity, includedRelationshipObject);
246254

247255
// we need to store the fact that this relationship was included in the payload
248256
// for EF, the repository will use these pointers to make ensure we don't try to
249257
// create resources if they already exist, we just need to create the relationship
250-
_jsonApiContext.HasOneRelationshipPointers.Add(attr, includedRelationshipObject);
258+
_jsonApiContext.HasOneRelationshipPointers.Add(hasOneAttr, includedRelationshipObject);
251259
}
252-
253-
return entity;
254260
}
255261

256262
private object SetHasManyRelationship(object entity,

test/UnitTests/Serialization/JsonApiDeSerializerTests.cs

+11-1
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,7 @@ public void Can_Deserialize_Independent_Side_Of_One_To_One_Relationship_With_Rel
240240
jsonApiContextMock.SetupAllProperties();
241241
jsonApiContextMock.Setup(m => m.ContextGraph).Returns(contextGraph);
242242
jsonApiContextMock.Setup(m => m.AttributesToUpdate).Returns(new Dictionary<AttrAttribute, object>());
243+
jsonApiContextMock.Setup(m => m.HasOneRelationshipPointers).Returns(new HasOneRelationshipPointers());
243244

244245
var jsonApiOptions = new JsonApiOptions();
245246
jsonApiContextMock.Setup(m => m.Options).Returns(jsonApiOptions);
@@ -255,7 +256,14 @@ public void Can_Deserialize_Independent_Side_Of_One_To_One_Relationship_With_Rel
255256
Id = "1",
256257
Attributes = new Dictionary<string, object> { { "property", property } },
257258
// a common case for this is deserialization in unit tests
258-
Relationships = new Dictionary<string, RelationshipData> { { "dependent", new RelationshipData { } } }
259+
Relationships = new Dictionary<string, RelationshipData> {
260+
{
261+
"dependent", new RelationshipData
262+
{
263+
SingleData = new ResourceIdentifierObject("dependents", "1")
264+
}
265+
}
266+
}
259267
}
260268
};
261269

@@ -267,6 +275,8 @@ public void Can_Deserialize_Independent_Side_Of_One_To_One_Relationship_With_Rel
267275
// assert
268276
Assert.NotNull(result);
269277
Assert.Equal(property, result.Property);
278+
Assert.NotNull(result.Dependent);
279+
Assert.Equal(1, result.Dependent.Id);
270280
}
271281

272282
[Fact]

0 commit comments

Comments
 (0)