Skip to content

Instantly share code, notes, and snippets.

@VictorKoenders
Created November 16, 2023 07:55
Show Gist options
  • Save VictorKoenders/5345d5eaef21629e3bb16c3750cb5818 to your computer and use it in GitHub Desktop.
Save VictorKoenders/5345d5eaef21629e3bb16c3750cb5818 to your computer and use it in GitHub Desktop.
[JsonMerge] attribute
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public class JsonMergeAttribute : Attribute
{
public JsonMergeAttribute(string propertyName)
{
PropertyName = propertyName;
}
public string PropertyName { get; }
}
public class JsonMergeConverter : JsonConverter<object>
{
public override bool CanConvert(Type typeToConvert)
{
return typeToConvert.GetCustomAttribute<JsonMergeAttribute>() != null;
}
public override object? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
throw new NotImplementedException();
}
public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options)
{
var attribute = value.GetType().GetCustomAttribute<JsonMergeAttribute>() ?? throw new Exception();
var members = value.GetType().GetProperties();
object? childObject = null;
writer.WriteStartObject();
foreach (var member in members)
{
if (member.Name == attribute.PropertyName)
{
childObject = member.GetValue(value);
continue;
}
writer.WritePropertyName(member.GetCustomAttribute<JsonPropertyNameAttribute>()?.Name ?? member.Name);
JsonSerializer.Serialize(writer, member.GetValue(value), member.PropertyType, options);
}
if (childObject == null) throw new Exception("Field \"" + attribute.PropertyName + "\" not found");
var obj = JsonSerializer.SerializeToDocument(childObject, options);
foreach (var kv in obj.RootElement.EnumerateObject())
{
writer.WritePropertyName(kv.Name);
kv.Value.WriteTo(writer);
}
writer.WriteEndObject();
}
}
public class JsonMergeSchemaFilter : ISchemaFilter
{
public void Apply(OpenApiSchema schema, SchemaFilterContext context)
{
var attribute = context.Type.GetCustomAttribute<JsonMergeAttribute>();
if (attribute == null) return;
var property = schema.Properties.First(p => string.Equals(attribute.PropertyName, p.Key, StringComparison.InvariantCultureIgnoreCase));
schema.Properties.Remove(property.Key);
Copy(schema.Properties, property.Value.Properties);
Copy(schema.AnyOf, property.Value.AnyOf);
Copy(schema.AllOf, property.Value.AllOf);
}
private static void Copy(IList<OpenApiSchema> dest, IList<OpenApiSchema> src)
{
foreach (var s in src)
dest.Add(s);
}
private static void Copy(IDictionary<string, OpenApiSchema> dest, IDictionary<string, OpenApiSchema> src)
{
foreach (var kv in src)
dest.Add(kv.Key, kv.Value);
}
}
// Merges the contents of the `Data` field into the parent object
[JsonMerge(nameof(Data))]
public class IdRev<T>
{
public Guid Id { get; set; }
public string Rev { get; set; }
public T Data { get; set; }
}
/* example:
record Foo(string Bar, string Baz);
// IdRev<Foo>:
{
"id": "...",
"rev": "...",
"bar": "...",
"baz": "..."
}
*/
builder.Services.AddControllers()
.AddJsonOptions(options =>
{
options.JsonSerializerOptions.Converters.Add(new JsonMergeConverter());
});
builder.Services.AddSwaggerGen(options =>
{
options.SchemaFilter<JsonMergeSchemaFilter>();
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment