C#

Catch incomplete order requests before they reach business logic

Separate missing fields, explicit nulls, and invalid business values so an AI-generated C# model enforces the contract your callers expect.

Published · AI disclosure

Suppose a partner sends order requests to a .NET application. The generated model declares a customer code and quantity, and the happy-path example deserializes successfully. That leaves several questions unanswered:

  • What happens when the partner omits the quantity?
  • Is a missing customer code treated differently from an explicit null?
  • Can a blank code or a negative quantity reach order processing?
  • Will changing the model break requests that existing callers still send?

Use one request type to settle these questions before applying the same rules across the API.

A C# declaration leaves part of the contract open

Consider this record:

C#
public sealed record SubmitOrder(string CustomerCode, int Quantity);

C# nullable-reference annotations guide compiler analysis; they do not themselves add runtime null checks. C# nullable-reference types

Constructor-based JSON deserialization also has its own rules. Without required-constructor enforcement, missing arguments can receive defaults. A constructor parameter with no C# default value does not, by itself, ensure the corresponding JSON field exists. System.Text.Json constructor requirements

That creates three separate decisions: whether a value must be supplied, whether it may be null, and whether it is valid for an order. The API contract should name all three.

How do we enforce presence and nullability?

The following example targets .NET 10 with nullable reference types enabled. Both options were introduced in .NET 9 and are opt-in; set them explicitly on the options used to read this contract.

The C# examples were checked against documentation; they have not been compiled or run in a .NET application.

C#
using System.Text.Json;

var options = new JsonSerializerOptions(JsonSerializerDefaults.Web)
{
    RespectRequiredConstructorParameters = true,
    RespectNullableAnnotations = true
};

string json = """{"customerCode":"ACME","quantity":2}""";
var order = JsonSerializer.Deserialize<SubmitOrder>(json, options)
    ?? throw new JsonException("An order object is required.");

if (string.IsNullOrWhiteSpace(order.CustomerCode) || order.Quantity <= 0)
    throw new ArgumentException("Supply a customer code and positive quantity.");

public sealed record SubmitOrder(string CustomerCode, int Quantity);

RespectRequiredConstructorParameters makes both non-optional constructor arguments mandatory in the JSON. RespectNullableAnnotations rejects an explicit null customer code. These checks are independent: enabling null enforcement alone does not require a missing field to appear. Required constructor parameters and nullable enforcement

For a property-based model, C# required or [JsonRequired] can require that a JSON property be present. Presence alone still permits null when the property's contract permits null. In particular, required string? expresses a value that must be supplied but may be null. Required properties

The final guard handles blank codes and non-positive quantities. Those are business rules beyond the serializer's presence and null checks. The null-coalescing guard handles a whole JSON document containing null, because nullable enforcement does not cover the root object.

Check the boundary your application actually uses

The snippet calls JsonSerializer directly. An ASP.NET Core controller also has model validation, and [ApiController] normally returns an automatic HTTP 400 for invalid model state. That is a separate stage with separate configuration; plain deserialization does not run MVC validation. ASP.NET Core model validation

Configure the serializer used by the endpoint, then map validation failures to the application's agreed client-error response. The illustrative ArgumentException above needs that handling; allowing it to escape from an endpoint is not an error-response design.

Two more boundaries matter when reviewing generated models:

  • Nullable enforcement does not cover collection elements or generic members. A List<string> declaration does not ensure every received element is non-null.
  • Constructor parameters must match the CLR property names and types. Renaming a JSON field with [JsonPropertyName] does not rename the constructor-binding target. Constructor binding

Prove the contract with request cases

Use the actual serializer settings to check these expected outcomes:

  • {"customerCode":"ACME","quantity":2} reaches business validation successfully.
  • {"customerCode":"ACME"} fails for missing quantity.
  • {"customerCode":null,"quantity":2} fails null enforcement.
  • {"customerCode":" ","quantity":0} deserializes but fails the business guard.
  • null fails the explicit root-object guard.

Before tightening an existing contract, record current and proposed behavior for each payload, including known-valid requests from current callers. Have the API owner decide which changed rejections require caller updates before rollout. Test the configured endpoint's HTTP status, error body, and absence of writes for rejected requests; a serializer-only test cannot establish that boundary.

Your team can apply these settings and run the request cases directly. Consider an independent review when a contract change could interrupt an important partner integration. Keep it focused on the current-versus-proposed payload matrix, caller compatibility, and evidence from the actual endpoint.

Sources

Written with AI and not reviewed by a human. It may contain mistakes.