The WeChat custom menu API presents a challenging JSON structure that often leaves developers feeling overwhelmed. Consider this typical response:
{"menu":{"button":[{"type":"click","name":"Today's Song","key":"V1001_TODAY_MUSIC","sub_button":[]},{"type":"click","name":"Artist Bio","key":"V1001_TODAY_SINGER","sub_button":[]},{"name":"Menu","sub_button":[{"type":"view","name":"Search","url":"http://www.soso.com/","sub_button":[]},{"type":"view","name":"Video","url":"http://v.qq.com/","sub_button":[]},{"type":"click","name":"Like Us","key":"V1001_GOOD","sub_button":[]}]}]}}
The challenge lies in the numerous button types, each with different properties, plus the nested sub_button arrays. This complexity calls for an elegant solution.
- Defining the API Interface
Let's start by creating a simple API interface. For this example, we'll work with a MenuApi class:
public class MenuApi : ApiBase
{
private const string ApiName = "menu";
/// <summary>
/// Query custom menu
/// </summary>
public MenuResultModel Get()
{
var url = GetAccessApiUrl("get", ApiName);
return Get<MenuResultModel>(url, new MenuButtonConverter());
}
}
Key components here are MenuResultModel and MenuButtonConverter—their interplay handles the deserialization complexity.
Note: The ApiBase base class and Get method handle HTTP requests and JSON serialization. The method signature is:
protected T Get<T>(string url, params JsonConverter[] jsonConverts) where T : ApiResult
- Defining JSON Models
First, let's define the root model and menu information:
public class MenuResultModel : ApiResult
{
[JsonProperty("menu")]
public MenuData Menu { get; set; }
}
public class MenuData
{
[JsonProperty("button")]
public List<MenuButtonBase> Buttons { get; set; }
}
Now, let's define the menu button types using an enumeration for better maintainability:
public enum MenuButtonType
{
/// <summary>
/// Click event
/// </summary>
click = 1,
/// <summary>
/// URL redirect
/// </summary>
view = 2,
/// <summary>
/// Scan code push event
/// </summary>
scancode_push = 3,
/// <summary>
/// Scan code push with "message receiving" prompt
/// </summary>
scancode_waitmsg = 4,
/// <summary>
/// System photo capture
/// </summary>
pic_sysphoto = 5,
/// <summary>
/// Photo or album capture
/// </summary>
pic_photo_or_album = 6,
/// <summary>
/// WeChat photo picker
/// </summary>
pic_weixin = 7,
/// <summary>
/// Location selector
/// </summary>
location_select = 8,
/// <summary>
/// Send media message (except text)
/// </summary>
media_id = 9,
/// <summary>
/// Redirect to图文消息 URL
/// </summary>
view_limited = 10
}
Now let's create the base class for menu buttons:
public abstract class MenuButtonBase
{
/// <summary>
/// Menu title, max 16 bytes, submenu max 40 bytes
/// </summary>
[MaxLength(20)]
[JsonProperty("name")]
public virtual string Name { get; set; }
/// <summary>
/// Menu type (response action type)
/// </summary>
[JsonConverter(typeof(StringEnumConverter))]
[JsonProperty("type")]
public MenuButtonType ButtonType { get; set; }
}
Note: JsonProperty allows us to use camelCase naming in C# while mapping to different JSON property names. The StringEnumConverter handles converting string values to enum types.
Now let's define the submenu button type for menus containing child items:
public class SubMenuButton : MenuButtonBase
{
/// <summary>
/// Menu title, max 16 bytes, submenu max 40 bytes
/// </summary>
[MaxLength(8)]
[JsonProperty("name")]
public override string Name { get; set; }
/// <summary>
/// Submenu (secondary menu array, 1-5 items)
/// </summary>
[JsonProperty("sub_button")]
public List<MenuButtonBase> SubButtons { get; set; }
}
Now let's define specific button types. Here's the click button:
public class ClickButton : MenuButtonBase
{
public ClickButton()
{
this.ButtonType = MenuButtonType.click;
}
/// <summary>
/// Menu KEY value for message interface push, max 128 bytes
/// </summary>
[JsonProperty("key")]
public string Key { get; set; }
}
And the media ID button:
public class MediaIdButton : MenuButtonBase
{
public MediaIdButton()
{
this.ButtonType = MenuButtonType.media_id;
}
/// <summary>
/// Valid media_id returned from permanent material upload
/// </summary>
[JsonProperty("media_id")]
public string MediaId { get; set; }
}
Additional button types like ViewButton, ScancodePushButton, etc., would follow similar patterns.
- Implementing CustomCreationConverter
Now comes the core solution. The CustomCreationConverter from Json.NET allows us to create custom objects during deserialization based on the JSON content:
public class MenuButtonConverter : CustomCreationConverter<MenuButtonBase>
{
/// <summary>
/// Read JSON and create appropriate menu button type
/// </summary>
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (reader.TokenType == JsonToken.Null)
return null;
var jObject = JObject.Load(reader);
MenuButtonBase target = null;
// Get the type property
var typeProperty = jObject.Property("type");
if (typeProperty != null && typeProperty.Count > 0)
{
var typeValue = typeProperty.Value.ToString();
var buttonType = (MenuButtonType)Enum.Parse(typeof(MenuButtonType), typeValue);
#region Return corresponding menu type based on type
switch (buttonType)
{
case MenuButtonType.click:
target = new ClickButton();
break;
case MenuButtonType.view:
target = new ViewButton();
break;
case MenuButtonType.scancode_push:
target = new ScancodePushButton();
break;
case MenuButtonType.scancode_waitmsg:
target = new ScancodeWaitmsgButton();
break;
case MenuButtonType.pic_sysphoto:
target = new PicSysphotoButton();
break;
case MenuButtonType.pic_photo_or_album:
target = new PicPhotoOrAlbumButton();
break;
case MenuButtonType.pic_weixin:
target = new PicWeixinButton();
break;
case MenuButtonType.location_select:
target = new LocationSelectButton();
break;
case MenuButtonType.media_id:
target = new MediaIdButton();
break;
case MenuButtonType.view_limited:
target = new ViewLimitedButton();
break;
default:
throw new NotSupportedException($"Menu button type not supported: {buttonType}");
}
#endregion
}
else
{
// No type property means it's a submenu
target = new SubMenuButton();
}
// Populate the created object with remaining JSON properties
serializer.Populate(jObject.CreateReader(), target);
return target;
}
/// <summary>
/// Create object instance (will be populated by serializer)
/// </summary>
public override MenuButtonBase Create(Type objectType)
{
return new SubMenuButton();
}
}
The key insight is overriding the ReadJson method to inspect the "type" field in the JSON and instantiate the appropriate concrete class. If no type is present, we know it's a submenu container. The serializer.Populate call then fills in all the properties.
This approach cleanly handles the polymorphic nature of the WeChat menu API, allowing proper deserialization of the complex nested JSON structure into strongly-typed C# objects.