VSLive! Blog

Industry Insights, Information, and Developer News

Blog archive

Taming the Type Explosion: Managing Message Classes in REST-Heavy Applications

Building HonestCheetah.com (my GitHub project metrics tool) has taught me a lot of lessons, but one of the most annoying problems I've run into is what I call "message type sprawl." When you're making dozens of different REST and GraphQL calls to various APIs, you end up with an explosion of similar-but-not-quite-the-same classes for deserializing JSON responses.

Let me show you what I mean and how I've learned to manage it.

The Problem: Death by a Thousand DTOs
Here's what typically happens. You start with one API call:

// GitHub API: Get a repository
public class Repository 
{
    public string Name { get; set; }
    public string Owner { get; set; }
    public DateTime CreatedAt { get; set; }
    // ... 20 more properties
}

Then you need to call another endpoint that also returns repository data, but with slightly different fields:

// GitHub GraphQL: Repository in search results  
public class SearchRepository  // Can't call it Repository - name collision!
{
    public string Name { get; set; }
    public int StarCount { get; set; }
    public string Language { get; set; }
    // ... different set of properties
}

And another...with slightly different fields:

// GitHub API: Repository from user endpoint
public class UserRepository  // Running out of names...
{
    public string Name { get; set; }
    public bool IsPrivate { get; set; }
    public string DefaultBranch { get; set; }
    // ... yet another variation
}

Before you know it, you've got Repository, RepositoryInfo, RepositoryDetails, SearchRepository, UserRepository, ProjectRepository, and on and on. Your Models folder becomes a graveyard of slightly different DTO classes, and you're constantly playing the "which Repository class did I mean?" game.

The Solution: Response Classes with Inner Types
Here's the pattern I've settled on: create a Response class for each API call and use inner classes to define the message types specific to that response.

public class GetRepositoryResponse
{
    public Repository Data { get; set; }
    
    public class Repository
    {
        public string Name { get; set; }
        public string Owner { get; set; }
        public DateTime CreatedAt { get; set; }
        // Properties specific to this endpoint
    }
}

public class SearchRepositoriesResponse
{
    public List<Repository> Items { get; set; }
    public int TotalCount { get; set; }
    
    public class Repository
    {
        public string Name { get; set; }
        public int StarCount { get; set; }
        public string Language { get; set; }
        // Different properties for search results
    }
}

Now I can have multiple Repository classes without naming conflicts because they're scoped to their parent Response class:

// Usage is clear and unambiguous
var getRepoResponse = await GetRepository("benday-inc", "slnutil");
GetRepositoryResponse.Repository repo = getRepoResponse.Data;

var searchResponse = await SearchRepositories("slnutil");
SearchRepositoriesResponse.Repository firstResult = searchResponse.Items[0];

Benefits of This Approach

  • No More Naming Gymnastics: You don't need creative names like RepositoryDto, RepositoryModel, RepositoryInfo. Each response has its own Repository that matches exactly what that endpoint returns.
  • Clear API Contracts: Each Response class documents exactly what you get from each API call. The inner classes show the exact shape of the data for that specific endpoint.
  • Easier Maintenance: When GitHub changes what fields come back from an endpoint, you know exactly which class to update. It's right there in the Response class for that call.
  • IntelliSense That Makes Sense: Your IDE knows exactly which Repository you're working with based on the Response type. No more guessing or checking the definition.

The Catch: Types Aren't Interchangeable
Here's the thing that might feel weird at first: GetRepositoryResponse.Repository and SearchRepositoriesResponse.Repository are completely different types as far as C# is concerned. You can't pass one where the other is expected, even if they have similar properties.

// This won't compile
GetRepositoryResponse.Repository repo1 = getRepoResponse.Data;
SearchRepositoriesResponse.Repository repo2 = repo1; // Error!

This is actually a feature, not a bug. These really are different types from different API calls with different guarantees about what data is present.

If you need to work with common properties across different response types, consider:

// Option 1: Create an interface for shared properties
public interface IRepositoryBase
{
    string Name { get; }
    string Owner { get; }
}

// Have your inner classes implement it
public class GetRepositoryResponse
{
    public class Repository : IRepositoryBase
    {
        public string Name { get; set; }
        public string Owner { get; set; }
        // ... other properties
    }
}

// Option 2: Map to a common domain model
public class DomainRepository  // Your actual business object
{
    public string Name { get; set; }
    public string Owner { get; set; }
    
    public static DomainRepository FromGetResponse(GetRepositoryResponse.Repository repo)
    {
        return new DomainRepository 
        { 
            Name = repo.Name, 
            Owner = repo.Owner 
        };
    }
}

Automating the Boring Parts
Creating these Response classes by hand is tedious, especially when you're looking at a complex JSON response with nested objects. That's why I added a feature to my (free) slnutil tool to generate these classes automatically.

Install it:

dotnet tool install -g slnutil

Copy a sample JSON message result to the clipboard, then call slnutil from the command line:

slnutil classesfromjson /clipboard /innerclasses

It'll generate the Response class with all the inner classes properly structured based on your actual JSON. The /innerclasses flag is the key -- it creates the nested structure instead of flat classes. Of course, if you want to skip this inner class approach and use regular public types, you can do that, too by omitting the /innerclasses option.

When to Use This Pattern
This pattern works great when:

  • You're calling multiple endpoints that return similar-but-different data
  • You want clear separation between different API responses
  • You're working with third-party APIs where you don't control the response format
  • You need to support multiple versions of an API simultaneously

It might be overkill if:

  • You're only making a few API calls
  • Your API responses are very simple
  • You control both the client and server and can ensure consistency

Real-World Example from HonestCheetah
Here's an actual Response class from my GitHub metrics tool:

public class GetProjectItemsResponse
{
    public Organization Organization { get; set; }
    
    public class Organization
    {
        public ProjectV2 ProjectV2 { get; set; }
        
        public class ProjectV2
        {
            public string Title { get; set; }
            public ItemConnection Items { get; set; }
            
            public class ItemConnection
            {
                public PageInfo PageInfo { get; set; }
                public List<ProjectItem> Nodes { get; set; }
                
                public class PageInfo
                {
                    public bool HasNextPage { get; set; }
                    public string EndCursor { get; set; }
                }
                
                public class ProjectItem
                {
                    public string Id { get; set; }
                    public Content Content { get; set; }
                    
                    public class Content
                    {
                        public string Title { get; set; }
                        public int Number { get; set; }
                        public string State { get; set; }
                        // ... more fields
                    }
                }
            }
        }
    }
}

Yes, it's deeply nested. Yes, it's verbose. But it exactly matches the structure of the GraphQL response, there are no naming conflicts with my other 20+ Response classes, and when GitHub changes this endpoint, I know exactly where to make updates.

Conclusion
Message type sprawl is one of those problems that sneaks up on you. You start with a few DTOs, and before you know it, you're drowning in similarly-named classes that are almost impossible to keep straight. The Response class pattern with inner types isn't the only solution, but it's one that's served me well in building real applications that consume multiple APIs.

It keeps your types organized, avoids naming conflicts, and makes it crystal clear which data shapes belong to which API calls. And with tools like slnutil to generate the boilerplate, it's not even that much extra work.

Give it a try next time you find yourself creating RepositoryDto2 or UserResponseModelInfo. Your future self will thank you.


How do you manage message type explosion in your API-heavy applications? I'd love to hear about other patterns that work. Drop me a line at [email protected] or find me on YouTube.

About the Author

Benjamin Day is a consultant, trainer and author specializing in software development, project management and leadership.

Posted by Benjamin Day on 12/01/2025


Keep Up-to-Date with Visual Studio Live!

Email Address*Country*
Please type the letters/numbers you see above.