Search code examples
c#json.net

How to inject the JSON $schema value during Newtonsoft.JsonConvert.SerializeObject


I created a JSON schema for my C# code using:

// Create JSON schema
var generator = new JSchemaGenerator();
var schema = generator.Generate(typeof(ConfigFileJsonSchema));
schema.Title = "PlexCleaner Schema";
schema.Description = "PlexCleaner config file JSON schema";
schema.SchemaVersion = new Uri("http://json-schema.org/draft-06/schema");
schema.Id = new Uri("https://raw.githubusercontent.com/ptr727/PlexCleaner/main/PlexCleaner.schema.json");
Console.WriteLine(schema);

I want to add a reference to this scheme whenever I create JSON output from my code:

    private static string ToJson(ConfigFileJsonSchema settings)
    {
        return JsonConvert.SerializeObject(settings, Settings);
    }

    private static readonly JsonSerializerSettings Settings = new()
    {
        Formatting = Formatting.Indented,
        StringEscapeHandling = StringEscapeHandling.EscapeNonAscii,
        NullValueHandling = NullValueHandling.Ignore,
        // We expect containers to be cleared before deserializing
        // Make sure that collections are not read-only (get; set;) else deserialized values will be appended
        // https://stackoverflow.com/questions/35482896/clear-collections-before-adding-items-when-populating-existing-objects
        ObjectCreationHandling = ObjectCreationHandling.Replace
        // TODO: Add TraceWriter to log to Serilog
    };

How can I programmatically add the $schema URI to the created JSON, not meaning creating schema on the fly, but something like this:

public class ConfigFileJsonSchemaBase
{
    // Schema reference
    [JsonProperty(PropertyName = "$schema", Order = -2)]
    public string Schema { get; } = "https://raw.githubusercontent.com/ptr727/PlexCleaner/main/PlexCleaner.schema.json";

    // Default to 0 if no value specified, and always write the version first
    [DefaultValue(0)]
    [JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate, Order = -2)]
    public int SchemaVersion { get; set; } = ConfigFileJsonSchema.Version;
}

Without needing to add a $schema entry to the class. E.g. equivalent of:

schema.SchemaVersion = new Uri("http://json-schema.org/draft-06/schema");

There is a similar unanswered question: json serialization to refer schema


Solution

  • You can use a JsonConverter:

    public class SchemaJsonConverter : JsonConverter
    {
        private readonly string _schemaUrl;
    
        private readonly Type[] _types;
    
        public SchemaJsonConverter(string schemaUrl, params Type[] types)
        {
            this._schemaUrl = schemaUrl;
            this._types = types;
        }
    
        public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
        {
            JToken t = JToken.FromObject(value);
    
            if (t.Type != JTokenType.Object)
            {
                t.WriteTo(writer);
            }
            else
            {
                var o = (JObject)t;
                o.AddFirst(new JProperty("$Schema", this._schemaUrl));
                o.WriteTo(writer);
            }
        }
    
        public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
        {
            throw new NotImplementedException("Unnecessary because CanRead is false. The type will skip the converter.");
        }
    
        public override bool CanRead
        {
            get { return false; }
        }
    
        public override bool CanConvert(Type objectType)
        {
            return this._types.Any(t => t == objectType);
        }
    }
    

    You need the type to check the types affected by the converter and the schema url to inject it in your JSON. The converter allow you a fine control about the process of serialization.

    I use a simple class to test the converter:

    public class Something
    {
        public int Integer { get; set; }
        public string Text { get; set; }
    }
    

    And a method to run the sample:

    public static void Test()
    {
        var something = new Something
        {
            Integer = 37, 
            Text = "A text"
        };
    
        var settings = new JsonSerializerSettings
        {
            Formatting = Formatting.Indented,
            StringEscapeHandling = StringEscapeHandling.EscapeNonAscii,
            NullValueHandling = NullValueHandling.Ignore,
            // We expect containers to be cleared before deserializing
            // Make sure that collections are not read-only (get; set;) else deserialized values will be appended
            // https://stackoverflow.com/questions/35482896/clear-collections-before-adding-items-when-populating-existing-objects
            ObjectCreationHandling = ObjectCreationHandling.Replace
            // TODO: Add TraceWriter to log to Serilog
        };
    
        var schemaUrl = "http://json-schema.org/draft-06/schema";
        settings.Converters.Add(new SchemaJsonConverter(schemaUrl, something.GetType()));
    
        var json = JsonConvert.SerializeObject(something, settings);
        Console.WriteLine(json);
    }
    

    Output:

    {
      "$Schema": "http://json-schema.org/draft-06/schema",
      "Integer": 37,
      "Text": "A text"
    }
    

    UPDATE

    A static method for serialization:

    public static string SerializeJson(object obj, JsonSerializerSettings settings, string schemaUrl = null)
    {
        if (!string.IsNullOrEmpty(schemaUrl))
        {
            settings.Converters.Add(new SchemaJsonConverter(schemaUrl, obj.GetType()));
        }
    
        return JsonConvert.SerializeObject(obj, settings);
    }
    

    Usage:

    var json = SerializeJson(something, settings, schemaUrl);