In this post we are going to see how we can alter a json so that it is modified before serializing. We mainly do this right before sending the data to the user or client, since doing it internally does not make sense.
Index
1 - Initial Problem
What we're going to do in this post is to merge several posts I have already created. We're going to extend the Railway Oriented Programming library so it has the functionality to add translations to error messages, similar to what we saw in the localization and languages video.
Right now, if we use the Result<T>
structure in our API, it will return a Json like this in case of an error:
{
"Value":null,
"Errors":[
{
"Message":"Mensaje de error",
"ErrorCode":"d07777ac-b317-44cb-a585-fabd408f37bf"
}
],
"Success":false
}
And the code we use is basically something like this:
ResultDto<PlaceHolder> obj = Result.Failure<PlaceHolder>(Error.Create("Mensaje de error", GuidAleatoria));
But of course, there's a problem. If we want to respond in multiple languages, we can't, or at least not in a simple way.
2 - How to Create a Custom Serializer in C#
To do this, what we can do is make a custom Json serializer. This means that when we are going to serialize an element, instead of using the default serializer, it will use one created by us.
First, we are going to create a new project called ROP.ApiExtensions.Translations
, which will handle all the logic for these translations. It is separated so that if you need to copy it into your own project, you can do it easily.
Now let's create a class called ErrorDtoSerializer
, since the API responds with a DTO.
This class needs to accept a generic type, which will be the class that references the translation file.
And finally, it will extend the JsonConverter
class from the System.Text.json
package, which allows us to change the serializer. Once we have everything, our class will look like this:
public class ErrorDtoSerializer<TTranslationFile> : JsonConverter<ErrorDto>
{
public override ErrorDto Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
throw new NotImplementedException();
}
public override void Write(Utf8JsonWriter writer, ErrorDto value, JsonSerializerOptions options)
{
throw new NotImplementedException();
}
}
Read
-> converts from json toT
Write
-> converts fromT
to json.
In our case, we're only interested in the Write
method. Unfortunately, we can't leave Read
empty or call the base, so we must implement it as well.
2.1 - Convert from Type to Custom Json in C#
What we are going to do is modify the response so that, if the error does not contain an error message, it retrieves it from our translation file.
For this, we need access to IHttpContextAccessor
, which contains the language information the user is using. We'll get their cultureinfo, as we saw in the localization and language video. Note: all this part of the code is in that post.
public override void Write(Utf8JsonWriter writer, ErrorDto value, JsonSerializerOptions options)
{
string errorMessageValue = value.Message;
if (string.IsNullOrWhiteSpace(value.Message))
{
CultureInfo language = _httpContextAccessor.HttpContext.Request.Headers.GetCultureInfo();
errorMessageValue = LocalizationUtils<TTranslationFile>.GetValue(value.ErrorCode.ToString(), language);
}
}
Now we must construct the new message using the writer
type we receive:
public override void Write(Utf8JsonWriter writer, ErrorDto value, JsonSerializerOptions options)
{
string errorMessageValue = value.Message;
if (string.IsNullOrWhiteSpace(value.Message))
{
CultureInfo language = _httpContextAccessor.HttpContext.Request.Headers.GetCultureInfo();
errorMessageValue = LocalizationUtils<TTranslationFile>.GetValue(value.ErrorCode.ToString(), language);
}
writer.WriteStartObject();
writer.WriteString(nameof(Error.ErrorCode), value.ErrorCode.ToString());
writer.WriteString(nameof(Error.Message), errorMessageValue);
writer.WriteEndObject();
}
And that's it, now we have the translation ready.
2.2 - Convert from Json to Type in C#
Now we have to do the opposite, go from Json to a type:
public override ErrorDto Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
string errorMessage = null;
Guid errorCode = Guid.NewGuid();
while (reader.Read())
{
if (reader.TokenType == JsonTokenType.EndObject)
{
break;
}
if (reader.TokenType != JsonTokenType.PropertyName)
{
throw new JsonException();
}
string propertyName = reader.GetString();
if (propertyName == nameof(ErrorDto.ErrorCode))
{
reader.Read();
errorCode = Guid.Parse(reader.GetString() ?? string.Empty);
}
if (propertyName == nameof(ErrorDto.Message))
{
reader.Read();
errorMessage = reader.GetString();
}
}
//theoretically with the translation in place errormessage will never be null
if (errorMessage == null && errorCode == null)
throw new Exception("Either Message or the ErrorCode has to be populated into the error");
return new ErrorDto()
{
ErrorCode = errorCode,
Message = errorMessage
};
}
2.3 - Use a Custom Serializer in C#
With these changes, we have already created our custom serializer. Now we just need to use it.
To do this, it's as simple as adding it to our serialization instruction:
var serializeOptions = new JsonSerializerOptions();
serializeOptions.Converters.Add(new ErrorDtoSerializer<ErrorTranslations>(httpContextAccessorMock.Object));
And then, as we see in the test, we can use it manually
[Fact]
public void When_message_is_empty_then_translate()
{
Mock<IHeaderDictionary> mockHeader = new Mock<IHeaderDictionary>();
mockHeader.Setup(a => a["Accept-Language"]).Returns("en;q=0.4");
Mock<IHttpContextAccessor> httpContextAccessorMock = new Mock<IHttpContextAccessor>();
httpContextAccessorMock.Setup(a => a.HttpContext.Request.Headers).Returns(mockHeader.Object);
var serializeOptions = new JsonSerializerOptions();
serializeOptions.Converters.Add(new ErrorDtoSerializer<ErrorTranslations>(httpContextAccessorMock.Object));
ResultDto<Unit> obj = new ResultDto<Unit>()
{
Value = null,
Errors = new List<ErrorDto>()
{
new ErrorDto()
{
ErrorCode = ErrorTranslations.ErrorExample
}
}.ToImmutableArray()
};
string json = JsonSerializer.Serialize(obj, serializeOptions);
var resultDto = JsonSerializer.Deserialize<ResultDto<Unit>>(json);
Assert.Equal("This is the message Translated", resultDto.Errors.First().Message);
}
However, this doesn't make much sense; ideally, this should be done in a more or less automatic way.
2.4 - Include Custom JsonConverter in Services
What we can do is add it to the services, which will load the information when we start the application, which is what is really useful.
There are three ways to do this. The first two are the most "common", depending on the type of application you are building, you'll have the .AddJsonOptions()
method available, either through services.Addcontrollers().AddJsonOptions(...)
or services.AddMvcCore().AddJsonOptions(...)
.
And once there, add the following line.
services.AddControllers().AddJsonOptions(options =>
options.JsonSerializerOptions.Converters.Add(new ErrorDtoSerializer<TranslationFile>(httpContextAccessor)));
As we can see, it's quite a bit of code, so in the ROP library itself, there is an extension that allows you to do it easier:
services.AddControllers().AddJsonOptions(options =>
{
options.JsonSerializerOptions.AddTranslation<TraduccionErrores>(services);
} );
This way, its use is much easier.
And here we can see how it works in both languages.
Conclusion
In this post we've seen how to create a custom Json Serializer, where we can alter the result to return whatever suits our needs.
If there is any problem you can add a comment bellow or contact me in the website's contact form